1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
#include "rrdset-slots.h"
4
+#include "rrdset-pluginsd-array.h"
5
6
void rrdset_stream_send_chart_slot_assign(RRDSET *st) {
7
RRDHOST *host = st->rrdhost;
42
spinlock_unlock(&host->stream.snd.pluginsd_chart_slots.available.spinlock);
43
}
44
44
-void rrdset_pluginsd_receive_unslot(RRDSET *st) {
45
- for(size_t i = 0; i < st->pluginsd.size ;i++) {
46
- rrddim_acquired_release(st->pluginsd.prd_array[i].rda); // can be NULL
47
- st->pluginsd.prd_array[i].rda = NULL;
48
- st->pluginsd.prd_array[i].rd = NULL;
49
- st->pluginsd.prd_array[i].id = NULL;
45
+// --------------------------------------------------------------------------------------------------------------------
46
+// Helper function to release RRDDIM_ACQUIRED references in array entries
47
+// This must be called before the final prd_array_release when cleaning up
48
+
49
+static void prd_array_release_entries(PRD_ARRAY *arr) {
50
+ if (!arr)
51
+ return;
52
+
53
+ for (size_t i = 0; i < arr->size; i++) {
54
+ rrddim_acquired_release(arr->entries[i].rda); // safe with NULL
55
+ arr->entries[i].rda = NULL;
56
+ arr->entries[i].rd = NULL;
57
+ arr->entries[i].id = NULL;
58
}
59
+}
60
+
61
+static inline void rrdset_clear_host_chart_slot_mapping(RRDSET *st, int32_t last_slot) {
62
+ if(last_slot < 0)
63
+ return;
64
65
RRDHOST *host = st->rrdhost;
66
+ spinlock_lock(&host->stream.rcv.pluginsd_chart_slots.spinlock);
67
+ if((uint32_t)last_slot < host->stream.rcv.pluginsd_chart_slots.size &&
68
+ host->stream.rcv.pluginsd_chart_slots.array[last_slot] == st) {
69
+ host->stream.rcv.pluginsd_chart_slots.array[last_slot] = NULL;
70
+ }
71
+ spinlock_unlock(&host->stream.rcv.pluginsd_chart_slots.spinlock);
72
+}
73
+
74
+// --------------------------------------------------------------------------------------------------------------------
75
+// Unslot a chart - releases dimension references but keeps the array for reuse
76
+// This is called when switching charts, marking them obsolete, or during cleanup.
77
+//
78
+// Safe to call from:
79
+// - The collector thread itself (collector_tid == gettid_cached()): uses lock-free access
80
+// - Any thread when the collector is fully stopped (collector_tid == 0): uses refcount
81
+// Skips with a warning if a DIFFERENT thread's collector is active.
82
+
83
+void rrdset_pluginsd_receive_unslot(RRDSET *st) {
84
+ if(!st)
85
+ return;
86
+
87
+ RRDDIM_ACQUIRED **detached_rdas = NULL;
88
+ size_t detached_capacity = 0;
89
+ size_t detached_entries = 0;
90
+ bool we_are_collector = false;
91
+ PRD_ARRAY *arr = NULL;
92
+ int32_t last_slot = -1;
93
+
94
+ while(true) {
95
+ spinlock_lock(&st->pluginsd.spinlock);
96
+
97
+ // Check collector_tid inside spinlock
98
+ pid_t collector_tid = __atomic_load_n(&st->pluginsd.collector_tid, __ATOMIC_ACQUIRE);
99
+ we_are_collector = (collector_tid == gettid_cached());
100
+ bool different_collector_active = (collector_tid != 0 && !we_are_collector);
101
+
102
+ last_slot = st->pluginsd.last_slot;
103
+
104
+ if(different_collector_active) {
105
+ // Another thread is the active collector - we cannot safely touch the array.
106
+ // Keep pluginsd state unchanged in this path: the active collector may
107
+ // still read last_slot / dims_with_slots lock-free.
108
+ // Clear only the host slot mapping and bail out.
109
+ nd_log_limit_static_global_var(erl, 1, 0);
110
+ nd_log_limit(&erl, NDLS_DAEMON, NDLP_WARNING,
111
+ "PLUGINSD: rrdset_pluginsd_receive_unslot called while collector (tid %d) is active, skipping",
112
+ collector_tid);
113
+
114
+ spinlock_unlock(&st->pluginsd.spinlock);
115
+ freez(detached_rdas);
116
+ rrdset_clear_host_chart_slot_mapping(st, last_slot);
117
+ return;
118
+ }
119
+
120
+ // Either collector_tid == 0 (collector stopped) or collector_tid == our tid
121
+ // (we ARE the collector). In both cases, it's safe to detach dimension references.
122
+ arr = we_are_collector ?
123
+ prd_array_get_unsafe(&st->pluginsd.prd_array) :
124
+ prd_array_acquire_locked(&st->pluginsd.prd_array);
125
+
126
+ if(arr) {
127
+ if(!we_are_collector) {
128
+ // Verify no other thread holds an extra reference before clearing entries.
129
+ // After acquire_locked, refcount should be 2 (original + ours).
130
+ int32_t rc = __atomic_load_n(&arr->refcount, __ATOMIC_ACQUIRE);
131
+ internal_fatal(rc != 2,
132
+ "PRD_ARRAY: expected refcount 2 after acquire, got %d - concurrent reference leak", rc);
133
+
134
+ if(unlikely(rc != 2)) {
135
+ // Production guard: skip detachment when another reference is active.
136
+ // Clearing entries in this state can race and double-release references.
137
+ nd_log_limit_static_global_var(erl_rc, 1, 0);
138
+ nd_log_limit(&erl_rc, NDLS_DAEMON, NDLP_WARNING,
139
+ "PLUGINSD: unslot skipped for chart with unexpected PRD_ARRAY refcount %d (expected 2)",
140
+ rc);
141
+
142
+ prd_array_release(arr);
143
+ spinlock_unlock(&st->pluginsd.spinlock);
144
+ freez(detached_rdas);
145
+ rrdset_clear_host_chart_slot_mapping(st, last_slot);
146
+ return;
147
+ }
148
+ }
149
+
150
+ detached_entries = arr->size;
151
+ if(detached_entries > detached_capacity) {
152
+ // Allocate outside the spinlock to avoid allocator latency while other
153
+ // threads are spinning on this lock.
154
+ if(!we_are_collector)
155
+ prd_array_release(arr);
156
+
157
+ spinlock_unlock(&st->pluginsd.spinlock);
158
+
159
+ freez(detached_rdas);
160
+ detached_rdas = callocz(detached_entries, sizeof(*detached_rdas));
161
+ detached_capacity = detached_entries;
162
+ continue;
163
+ }
164
+
165
+ // Detach entries while holding st->pluginsd.spinlock so concurrent unslot/cleanup
166
+ // cannot race and release the same RRDDIM_ACQUIRED pointers twice.
167
+ for(size_t i = 0; i < detached_entries; i++) {
168
+ detached_rdas[i] = arr->entries[i].rda;
169
+ arr->entries[i].rda = NULL;
170
+ arr->entries[i].rd = NULL;
171
+ arr->entries[i].id = NULL;
172
+ }
173
+ }
174
+ else
175
+ detached_entries = 0;
176
+
177
+ st->pluginsd.last_slot = -1;
178
+ st->pluginsd.dims_with_slots = false;
179
54
- if(st->pluginsd.last_slot >= 0 &&
55
- (uint32_t)st->pluginsd.last_slot < host->stream.rcv.pluginsd_chart_slots.size &&
56
- host->stream.rcv.pluginsd_chart_slots.array[st->pluginsd.last_slot] == st) {
57
- host->stream.rcv.pluginsd_chart_slots.array[st->pluginsd.last_slot] = NULL;
180
+ spinlock_unlock(&st->pluginsd.spinlock);
181
+ break;
182
}
183
60
- st->pluginsd.last_slot = -1;
61
- st->pluginsd.dims_with_slots = false;
184
+ // Release detached references outside the spinlock.
185
+ if(detached_rdas) {
186
+ for(size_t i = 0; i < detached_entries; i++)
187
+ rrddim_acquired_release(detached_rdas[i]); // safe with NULL
188
+
189
+ freez(detached_rdas);
190
+ }
191
+
192
+ if(arr && !we_are_collector) {
193
+ // Release our acquired reference (keeps the struct's reference alive for reuse)
194
+ prd_array_release(arr);
195
+ }
196
+
197
+ rrdset_clear_host_chart_slot_mapping(st, last_slot);
198
}
199
200
+// --------------------------------------------------------------------------------------------------------------------
201
+// Full cleanup - unslots and frees the array
202
+// This is called during chart finalization or host cleanup
203
+// Thread-safe: uses spinlock for cleanup coordination and reference counting for array lifetime
204
+
205
void rrdset_pluginsd_receive_unslot_and_cleanup(RRDSET *st) {
206
if(!st)
207
return;
208
209
spinlock_lock(&st->pluginsd.spinlock);
210
70
- rrdset_pluginsd_receive_unslot(st);
211
+ // Check if collector is still active.
212
+ pid_t collector_tid = __atomic_load_n(&st->pluginsd.collector_tid, __ATOMIC_ACQUIRE);
213
+ pid_t current_tid = gettid_cached();
214
+ if(collector_tid != 0) {
215
+ if(collector_tid != current_tid &&
216
+ !rrdset_flag_check(st, RRDSET_FLAG_COLLECTION_FINISHED)) {
217
+ internal_fatal(true,
218
+ "PRD_ARRAY: cleanup called while collector (tid %d) is still active - lifecycle violation",
219
+ collector_tid);
220
+
221
+ nd_log_limit_static_global_var(erl, 1, 0);
222
+ nd_log_limit(&erl, NDLS_DAEMON, NDLP_WARNING,
223
+ "PLUGINSD: attempted cleanup while collector (tid %d) is still active on chart, skipping",
224
+ collector_tid);
225
+ spinlock_unlock(&st->pluginsd.spinlock);
226
+ return;
227
+ }
228
+
229
+ if(collector_tid == current_tid) {
230
+ // Cleanup in the collector thread should not normally happen.
231
+ // Keep this explicit so we don't mask it as a stale tid case.
232
+#ifdef NETDATA_INTERNAL_CHECKS
233
+ internal_fatal(true,
234
+ "PRD_ARRAY: cleanup called from collector thread (tid %d) - lifecycle violation",
235
+ collector_tid);
236
+#endif
237
+
238
+ nd_log_limit_static_global_var(erl_collector, 1, 0);
239
+ nd_log_limit(&erl_collector, NDLS_DAEMON, NDLP_WARNING,
240
+ "PLUGINSD: cleanup called from collector thread (tid %d), forcing collector_tid=0",
241
+ collector_tid);
242
+ }
243
+ else {
244
+ // Finalization can hit stale collector_tid on charts that were not switched away.
245
+ // Treat this as cleanup ownership handoff and continue.
246
+ nd_log_limit_static_global_var(erl_finalize, 1, 0);
247
+ nd_log_limit(&erl_finalize, NDLS_DAEMON, NDLP_WARNING,
248
+ "PLUGINSD: cleanup forcing stale collector_tid=%d to 0 for finalized chart",
249
+ collector_tid);
250
+ }
251
+
252
+ __atomic_store_n(&st->pluginsd.collector_tid, 0, __ATOMIC_RELEASE);
253
+ }
254
+
255
+ // Replace the array with NULL - this prevents new references from being acquired
256
+ PRD_ARRAY *old_arr = prd_array_replace(&st->pluginsd.prd_array, NULL);
257
+
258
+ // Capture last_slot before resetting - we need it to clear the host mapping
259
+ int32_t last_slot = st->pluginsd.last_slot;
260
72
- rrd_slot_memory_removed(st->pluginsd.size * sizeof(struct pluginsd_rrddim));
73
- freez(st->pluginsd.prd_array);
74
- st->pluginsd.prd_array = NULL;
75
- st->pluginsd.size = 0;
76
- st->pluginsd.pos = 0;
261
+ // Reset state while holding the lock
262
+ __atomic_store_n(&st->pluginsd.pos, 0, __ATOMIC_RELAXED);
263
st->pluginsd.set = false;
264
st->pluginsd.last_slot = -1;
265
st->pluginsd.dims_with_slots = false;
80
- st->pluginsd.collector_tid = 0;
266
267
spinlock_unlock(&st->pluginsd.spinlock);
268
+
269
+ // Clear the chart slot mapping using the captured last_slot value
270
+ rrdset_clear_host_chart_slot_mapping(st, last_slot);
271
+
272
+ // Now handle the old array outside the lock
273
+ if (old_arr) {
274
+ // After prd_array_replace, we hold the only reference (refcount should be 1).
275
+ // It's safe to release entries only when we're the sole owner, to avoid clearing
276
+ // entries that another thread might still be reading through its own reference.
277
+ int32_t rc = __atomic_load_n(&old_arr->refcount, __ATOMIC_ACQUIRE);
278
+ internal_fatal(rc != 1,
279
+ "PRD_ARRAY: expected refcount 1 after replace, got %d - concurrent reference leak", rc);
280
+
281
+ if(unlikely(rc != 1)) {
282
+ // Production guard: another reference still exists, so clearing entries
283
+ // here could race with readers and double-release RRDDIM_ACQUIRED.
284
+ nd_log_limit_static_global_var(erl_cleanup_rc, 1, 0);
285
+ nd_log_limit(&erl_cleanup_rc, NDLS_DAEMON, NDLP_WARNING,
286
+ "PLUGINSD: cleanup deferred for chart with unexpected PRD_ARRAY refcount %d (expected 1)",
287
+ rc);
288
+
289
+ // Drop our reference only; remaining owners will eventually release.
290
+ prd_array_release(old_arr);
291
+ return;
292
+ }
293
+
294
+ // Release all dimension references (safe - we're the sole owner).
295
+ prd_array_release_entries(old_arr);
296
+
297
+ // Release our reference - this will free the array (refcount 1 -> 0)
298
+ prd_array_release(old_arr);
299
+ }
300
}
301
302
+// --------------------------------------------------------------------------------------------------------------------
303
+// Initialize the pluginsd slots for a chart
304
+
305
void rrdset_pluginsd_receive_slots_initialize(RRDSET *st) {
306
spinlock_init(&st->pluginsd.spinlock);
307
st->pluginsd.last_slot = -1;
308
+ st->pluginsd.prd_array = NULL; // Explicitly initialize to NULL
309
+}
310
+
311
+// --------------------------------------------------------------------------------------------------------------------
312
+// Stress test for PRD_ARRAY lifecycle separation model
313
+// Run with: netdata -W prd-array-stress
314
+//
315
+// This test validates the lifecycle separation model used in production:
316
+// - In production, the collector is FULLY STOPPED before cleanup runs
317
+// - The collector_tid check is a safety mechanism, but the real protection comes from lifecycle separation
318
+// - This test simulates that by running the writer and cleaner in non-overlapping phases
319
+//
320
+// The test runs in cycles:
321
+// 1. Writer phase: collector runs multiple iterations (collector_tid set)
322
+// 2. Handoff: collector fully stops (collector_tid cleared, writer_done signaled)
323
+// 3. Cleanup phase: cleaner runs (only when writer is fully stopped)
324
+// 4. Repeat
325
+// --------------------------------------------------------------------------------------------------------------------
326
+
327
+#define PRD_STRESS_TEST_DURATION_SEC 5
328
+#define PRD_STRESS_ITERATIONS_PER_PHASE 50
329
+
330
+typedef struct {
331
+ PRD_ARRAY *prd_array;
332
+ pid_t collector_tid;
333
+ SPINLOCK spinlock;
334
+
335
+ // Lifecycle coordination (simulates stream receiver stop/start)
336
+ bool test_running; // Overall test is running
337
+ bool writer_should_run; // Writer is allowed to run
338
+ bool writer_is_running; // Writer is currently in a phase
339
+
340
+ // Counters
341
+ uint64_t grow_count;
342
+ uint64_t cleanup_count;
343
+ uint64_t phase_count;
344
+} prd_stress_state_t;
345
+
346
+static prd_stress_state_t prd_stress_state;
347
+
348
+static void prd_stress_writer_thread(void *arg __maybe_unused) {
349
+ while (__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE)) {
350
+
351
+ // Wait until we're allowed to run (simulates stream receiver starting)
352
+ while (__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE) &&
353
+ !__atomic_load_n(&prd_stress_state.writer_should_run, __ATOMIC_ACQUIRE)) {
354
+ tinysleep();
355
+ }
356
+
357
+ if (!__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE))
358
+ break;
359
+
360
+ // Signal that writer is now running
361
+ __atomic_store_n(&prd_stress_state.writer_is_running, true, __ATOMIC_RELEASE);
362
+
363
+ // Simulate collector_tid being set (like pluginsd_set_scope_chart does)
364
+ __atomic_store_n(&prd_stress_state.collector_tid, gettid_cached(), __ATOMIC_RELEASE);
365
+
366
+ // Run multiple iterations in this phase (simulates collecting data)
367
+ for (int iter = 0; iter < PRD_STRESS_ITERATIONS_PER_PHASE; iter++) {
368
+ if (!__atomic_load_n(&prd_stress_state.writer_should_run, __ATOMIC_ACQUIRE))
369
+ break;
370
+
371
+ PRD_ARRAY *current_arr = prd_array_get_unsafe(&prd_stress_state.prd_array);
372
+
373
+ size_t current_size = current_arr ? current_arr->size : 0;
374
+ size_t new_size = current_size + 10;
375
+
376
+ if (new_size > 500)
377
+ new_size = 10;
378
+
379
+ PRD_ARRAY *new_arr = prd_array_create(new_size);
380
+
381
+ if (current_arr && current_size > 0) {
382
+ size_t copy_count = (current_size < new_size) ? current_size : new_size;
383
+ for(size_t i = 0; i < copy_count; i++) {
384
+ new_arr->entries[i].rd = current_arr->entries[i].rd;
385
+ new_arr->entries[i].id = current_arr->entries[i].id;
386
+ new_arr->entries[i].rda = NULL;
387
+ }
388
+ }
389
+
390
+ for (size_t i = (current_size < new_size ? current_size : 0); i < new_size; i++) {
391
+ new_arr->entries[i].rd = (void *)(uintptr_t)(i + 1);
392
+ new_arr->entries[i].id = "test";
393
+ }
394
+
395
+ PRD_ARRAY *old_arr = prd_array_replace(&prd_stress_state.prd_array, new_arr);
396
+
397
+ if (old_arr)
398
+ prd_array_release(old_arr);
399
+
400
+ __atomic_fetch_add(&prd_stress_state.grow_count, 1, __ATOMIC_RELAXED);
401
+
402
+ tinysleep();
403
+ }
404
+
405
+ // Clear collector_tid (like pluginsd_set_scope_chart does when switching away)
406
+ __atomic_store_n(&prd_stress_state.collector_tid, 0, __ATOMIC_RELEASE);
407
+
408
+ // Signal that writer phase is complete
409
+ __atomic_store_n(&prd_stress_state.writer_is_running, false, __ATOMIC_RELEASE);
410
+
411
+ // Wait until controller signals us to run again
412
+ while (__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE) &&
413
+ !__atomic_load_n(&prd_stress_state.writer_should_run, __ATOMIC_ACQUIRE)) {
414
+ tinysleep();
415
+ }
416
+ }
417
+}
418
+
419
+static void prd_stress_cleanup_thread(void *arg __maybe_unused) {
420
+ while (__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE)) {
421
+
422
+ // Wait until writer is fully stopped (simulates stream_receiver_signal_to_stop_and_wait)
423
+ while (__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE) &&
424
+ __atomic_load_n(&prd_stress_state.writer_is_running, __ATOMIC_ACQUIRE)) {
425
+ tinysleep();
426
+ }
427
+
428
+ if (!__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE))
429
+ break;
430
+
431
+ // Now safe to cleanup - writer is fully stopped
432
+ spinlock_lock(&prd_stress_state.spinlock);
433
+
434
+ // Double-check collector_tid (should be 0 since writer stopped)
435
+ pid_t collector_tid = __atomic_load_n(&prd_stress_state.collector_tid, __ATOMIC_ACQUIRE);
436
+ if (collector_tid != 0) {
437
+ // This shouldn't happen if lifecycle is correct
438
+ spinlock_unlock(&prd_stress_state.spinlock);
439
+ continue;
440
+ }
441
+
442
+ PRD_ARRAY *old_arr = prd_array_replace(&prd_stress_state.prd_array, NULL);
443
+
444
+ spinlock_unlock(&prd_stress_state.spinlock);
445
+
446
+ if (old_arr) {
447
+ for (size_t i = 0; i < old_arr->size; i++) {
448
+ old_arr->entries[i].rda = NULL;
449
+ old_arr->entries[i].rd = NULL;
450
+ old_arr->entries[i].id = NULL;
451
+ }
452
+
453
+ prd_array_release(old_arr);
454
+ __atomic_fetch_add(&prd_stress_state.cleanup_count, 1, __ATOMIC_RELAXED);
455
+ }
456
+
457
+ tinysleep();
458
+ }
459
+}
460
+
461
+// Controller thread - orchestrates the lifecycle phases
462
+static void prd_stress_controller_thread(void *arg __maybe_unused) {
463
+ while (__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE)) {
464
+
465
+ // Start writer phase
466
+ __atomic_store_n(&prd_stress_state.writer_should_run, true, __ATOMIC_RELEASE);
467
+
468
+ // Wait for writer to start and run
469
+ sleep_usec(10000); // 10ms - let writer run
470
+
471
+ // Signal writer to stop (simulates stream receiver stopping)
472
+ __atomic_store_n(&prd_stress_state.writer_should_run, false, __ATOMIC_RELEASE);
473
+
474
+ // Wait for writer to fully stop
475
+ while (__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE) &&
476
+ __atomic_load_n(&prd_stress_state.writer_is_running, __ATOMIC_ACQUIRE)) {
477
+ tinysleep();
478
+ }
479
+
480
+ // Cleanup phase - cleaner will run now that writer is stopped
481
+ sleep_usec(5000); // 5ms - let cleanup run
482
+
483
+ __atomic_fetch_add(&prd_stress_state.phase_count, 1, __ATOMIC_RELAXED);
484
+ }
485
+}
486
+
487
+int prd_array_stress_test(void) {
488
+ int duration_secs = PRD_STRESS_TEST_DURATION_SEC;
489
+
490
+ fprintf(stderr, "\nPRD_ARRAY Lifecycle Stress Test\n");
491
+ fprintf(stderr, "================================\n");
492
+ fprintf(stderr, "Duration: %d seconds\n", duration_secs);
493
+ fprintf(stderr, "This test simulates production lifecycle:\n");
494
+ fprintf(stderr, " 1. Writer (collector) runs with collector_tid set\n");
495
+ fprintf(stderr, " 2. Writer fully stops (collector_tid cleared)\n");
496
+ fprintf(stderr, " 3. Cleaner runs cleanup\n");
497
+ fprintf(stderr, " 4. Repeat\n\n");
498
+
499
+ // Initialize state
500
+ memset(&prd_stress_state, 0, sizeof(prd_stress_state));
501
+ prd_stress_state.prd_array = prd_array_create(10);
502
+ spinlock_init(&prd_stress_state.spinlock);
503
+ __atomic_store_n(&prd_stress_state.test_running, true, __ATOMIC_RELEASE);
504
+
505
+ // Start threads
506
+ char thread_name[32];
507
+
508
+ snprintfz(thread_name, sizeof(thread_name), "PRDSTRESS_W");
509
+ ND_THREAD *writer_thread = nd_thread_create(thread_name, NETDATA_THREAD_OPTION_DEFAULT,
510
+ prd_stress_writer_thread, NULL);
511
+
512
+ snprintfz(thread_name, sizeof(thread_name), "PRDSTRESS_C");
513
+ ND_THREAD *cleanup_thread = nd_thread_create(thread_name, NETDATA_THREAD_OPTION_DEFAULT,
514
+ prd_stress_cleanup_thread, NULL);
515
+
516
+ snprintfz(thread_name, sizeof(thread_name), "PRDSTRESS_CTRL");
517
+ ND_THREAD *controller_thread = nd_thread_create(thread_name, NETDATA_THREAD_OPTION_DEFAULT,
518
+ prd_stress_controller_thread, NULL);
519
+
520
+ // Run the test
521
+ fprintf(stderr, "Running stress test...\n");
522
+ for (int i = 0; i < duration_secs; i++) {
523
+ sleep_usec(USEC_PER_SEC);
524
+ fprintf(stderr, " %d/%d sec - phases: %"PRIu64", grows: %"PRIu64", cleanups: %"PRIu64"\n",
525
+ i + 1, duration_secs,
526
+ __atomic_load_n(&prd_stress_state.phase_count, __ATOMIC_RELAXED),
527
+ __atomic_load_n(&prd_stress_state.grow_count, __ATOMIC_RELAXED),
528
+ __atomic_load_n(&prd_stress_state.cleanup_count, __ATOMIC_RELAXED));
529
+ }
530
+
531
+ // Stop all threads
532
+ __atomic_store_n(&prd_stress_state.test_running, false, __ATOMIC_RELEASE);
533
+ __atomic_store_n(&prd_stress_state.writer_should_run, true, __ATOMIC_RELEASE); // Unblock writer
534
+
535
+ nd_thread_join(controller_thread);
536
+ nd_thread_join(writer_thread);
537
+ nd_thread_join(cleanup_thread);
538
+
539
+ // Final cleanup
540
+ PRD_ARRAY *final_arr = prd_array_replace(&prd_stress_state.prd_array, NULL);
541
+ if (final_arr)
542
+ prd_array_release(final_arr);
543
+
544
+ // Print results
545
+ fprintf(stderr, "\nTest completed!\n");
546
+ fprintf(stderr, "===============\n");
547
+ fprintf(stderr, "Total phases: %"PRIu64"\n", prd_stress_state.phase_count);
548
+ fprintf(stderr, "Total grows: %"PRIu64"\n", prd_stress_state.grow_count);
549
+ fprintf(stderr, "Total cleanups: %"PRIu64"\n", prd_stress_state.cleanup_count);
550
+
551
+ if (prd_stress_state.cleanup_count > 0 && prd_stress_state.grow_count > 0) {
552
+ fprintf(stderr, "\nSUCCESS: Lifecycle separation validated\n");
553
+ fprintf(stderr, "- Writer and cleaner ran in non-overlapping phases\n");
554
+ fprintf(stderr, "- No concurrent access to the array\n");
555
+ fprintf(stderr, "- Reference counting worked correctly\n");
556
+ return 0;
557
+ } else {
558
+ fprintf(stderr, "\nWARNING: Low activity - increase test duration\n");
559
+ return 1;
560
+ }
561
}