| 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 |