master
c 557 lines 23.4 KB
Raw
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;
8 spinlock_lock(&host->stream.snd.pluginsd_chart_slots.available.spinlock);
9
10 if(host->stream.snd.pluginsd_chart_slots.available.used > 0)
11 st->stream.snd.chart_slot =
12 host->stream.snd.pluginsd_chart_slots.available.array[--host->stream.snd.pluginsd_chart_slots.available.used];
13 else
14 st->stream.snd.chart_slot = ++host->stream.snd.pluginsd_chart_slots.last_used;
15
16 spinlock_unlock(&host->stream.snd.pluginsd_chart_slots.available.spinlock);
17 }
18
19 void rrdset_stream_send_chart_slot_release(RRDSET *st) {
20 if(!st->stream.snd.chart_slot || st->rrdhost->stream.snd.pluginsd_chart_slots.available.ignore)
21 return;
22
23 RRDHOST *host = st->rrdhost;
24 spinlock_lock(&host->stream.snd.pluginsd_chart_slots.available.spinlock);
25
26 if(host->stream.snd.pluginsd_chart_slots.available.used >= host->stream.snd.pluginsd_chart_slots.available.size) {
27 uint32_t old_slots = host->stream.snd.pluginsd_chart_slots.available.size;
28 uint32_t new_slots = (old_slots > 0) ? (old_slots * 2) : 1024;
29
30 host->stream.snd.pluginsd_chart_slots.available.array =
31 reallocz(host->stream.snd.pluginsd_chart_slots.available.array, new_slots * sizeof(uint32_t));
32
33 host->stream.snd.pluginsd_chart_slots.available.size = new_slots;
34
35 rrd_slot_memory_added((new_slots - old_slots) * sizeof(uint32_t));
36 }
37
38 host->stream.snd.pluginsd_chart_slots.available.array[host->stream.snd.pluginsd_chart_slots.available.used++] =
39 st->stream.snd.chart_slot;
40
41 st->stream.snd.chart_slot = 0;
42 spinlock_unlock(&host->stream.snd.pluginsd_chart_slots.available.spinlock);
43 }
44
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
180 spinlock_unlock(&st->pluginsd.spinlock);
181 break;
182 }
183
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
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 // A different thread owns this chart as collector.
217 // We must not release PRD dimension references while that thread may
218 // still be using cached rd pointers from the array.
219 // RRDSET_FLAG_COLLECTION_FINISHED is not a reliable signal here:
220 // it can be set by the cleanup caller (service thread) rather than
221 // by the collector itself, creating a race where we free dimensions
222 // that the collector is actively dereferencing.
223 // Legitimate teardown clears collector_tid via
224 // rrdhost_pluginsd_receive_chart_slots_free() after the receiver
225 // is fully stopped, before invoking cleanup.
226 nd_log_limit_static_global_var(erl, 1, 0);
227 nd_log_limit(&erl, NDLS_DAEMON, NDLP_WARNING,
228 "PLUGINSD: attempted cleanup while collector (tid %d) is still active on chart, skipping",
229 collector_tid);
230 spinlock_unlock(&st->pluginsd.spinlock);
231 return;
232 }
233
234 // We ARE the collector thread - safe to proceed with cleanup.
235 // This should not normally happen; keep it explicit so we don't
236 // mask it as a stale tid case.
237 #ifdef NETDATA_INTERNAL_CHECKS
238 internal_fatal(true,
239 "PRD_ARRAY: cleanup called from collector thread (tid %d) - lifecycle violation",
240 collector_tid);
241 #endif
242
243 nd_log_limit_static_global_var(erl_collector, 1, 0);
244 nd_log_limit(&erl_collector, NDLS_DAEMON, NDLP_WARNING,
245 "PLUGINSD: cleanup called from collector thread (tid %d), forcing collector_tid=0",
246 collector_tid);
247
248 __atomic_store_n(&st->pluginsd.collector_tid, 0, __ATOMIC_RELEASE);
249 }
250
251 // Replace the array with NULL - this prevents new references from being acquired
252 PRD_ARRAY *old_arr = prd_array_replace(&st->pluginsd.prd_array, NULL);
253
254 // Capture last_slot before resetting - we need it to clear the host mapping
255 int32_t last_slot = st->pluginsd.last_slot;
256
257 // Reset state while holding the lock
258 __atomic_store_n(&st->pluginsd.pos, 0, __ATOMIC_RELAXED);
259 st->pluginsd.set = false;
260 st->pluginsd.last_slot = -1;
261 st->pluginsd.dims_with_slots = false;
262
263 spinlock_unlock(&st->pluginsd.spinlock);
264
265 // Clear the chart slot mapping using the captured last_slot value
266 rrdset_clear_host_chart_slot_mapping(st, last_slot);
267
268 // Now handle the old array outside the lock
269 if (old_arr) {
270 // After prd_array_replace, we hold the only reference (refcount should be 1).
271 // It's safe to release entries only when we're the sole owner, to avoid clearing
272 // entries that another thread might still be reading through its own reference.
273 int32_t rc = __atomic_load_n(&old_arr->refcount, __ATOMIC_ACQUIRE);
274 internal_fatal(rc != 1,
275 "PRD_ARRAY: expected refcount 1 after replace, got %d - concurrent reference leak", rc);
276
277 if(unlikely(rc != 1)) {
278 // Production guard: another reference still exists, so clearing entries
279 // here could race with readers and double-release RRDDIM_ACQUIRED.
280 nd_log_limit_static_global_var(erl_cleanup_rc, 1, 0);
281 nd_log_limit(&erl_cleanup_rc, NDLS_DAEMON, NDLP_WARNING,
282 "PLUGINSD: cleanup deferred for chart with unexpected PRD_ARRAY refcount %d (expected 1)",
283 rc);
284
285 // Drop our reference only; remaining owners will eventually release.
286 prd_array_release(old_arr);
287 return;
288 }
289
290 // Release all dimension references (safe - we're the sole owner).
291 prd_array_release_entries(old_arr);
292
293 // Release our reference - this will free the array (refcount 1 -> 0)
294 prd_array_release(old_arr);
295 }
296 }
297
298 // --------------------------------------------------------------------------------------------------------------------
299 // Initialize the pluginsd slots for a chart
300
301 void rrdset_pluginsd_receive_slots_initialize(RRDSET *st) {
302 spinlock_init(&st->pluginsd.spinlock);
303 st->pluginsd.last_slot = -1;
304 st->pluginsd.prd_array = NULL; // Explicitly initialize to NULL
305 }
306
307 // --------------------------------------------------------------------------------------------------------------------
308 // Stress test for PRD_ARRAY lifecycle separation model
309 // Run with: netdata -W prd-array-stress
310 //
311 // This test validates the lifecycle separation model used in production:
312 // - In production, the collector is FULLY STOPPED before cleanup runs
313 // - The collector_tid check is a safety mechanism, but the real protection comes from lifecycle separation
314 // - This test simulates that by running the writer and cleaner in non-overlapping phases
315 //
316 // The test runs in cycles:
317 // 1. Writer phase: collector runs multiple iterations (collector_tid set)
318 // 2. Handoff: collector fully stops (collector_tid cleared, writer_done signaled)
319 // 3. Cleanup phase: cleaner runs (only when writer is fully stopped)
320 // 4. Repeat
321 // --------------------------------------------------------------------------------------------------------------------
322
323 #define PRD_STRESS_TEST_DURATION_SEC 5
324 #define PRD_STRESS_ITERATIONS_PER_PHASE 50
325
326 typedef struct {
327 PRD_ARRAY *prd_array;
328 pid_t collector_tid;
329 SPINLOCK spinlock;
330
331 // Lifecycle coordination (simulates stream receiver stop/start)
332 bool test_running; // Overall test is running
333 bool writer_should_run; // Writer is allowed to run
334 bool writer_is_running; // Writer is currently in a phase
335
336 // Counters
337 uint64_t grow_count;
338 uint64_t cleanup_count;
339 uint64_t phase_count;
340 } prd_stress_state_t;
341
342 static prd_stress_state_t prd_stress_state;
343
344 static void prd_stress_writer_thread(void *arg __maybe_unused) {
345 while (__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE)) {
346
347 // Wait until we're allowed to run (simulates stream receiver starting)
348 while (__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE) &&
349 !__atomic_load_n(&prd_stress_state.writer_should_run, __ATOMIC_ACQUIRE)) {
350 tinysleep();
351 }
352
353 if (!__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE))
354 break;
355
356 // Signal that writer is now running
357 __atomic_store_n(&prd_stress_state.writer_is_running, true, __ATOMIC_RELEASE);
358
359 // Simulate collector_tid being set (like pluginsd_set_scope_chart does)
360 __atomic_store_n(&prd_stress_state.collector_tid, gettid_cached(), __ATOMIC_RELEASE);
361
362 // Run multiple iterations in this phase (simulates collecting data)
363 for (int iter = 0; iter < PRD_STRESS_ITERATIONS_PER_PHASE; iter++) {
364 if (!__atomic_load_n(&prd_stress_state.writer_should_run, __ATOMIC_ACQUIRE))
365 break;
366
367 PRD_ARRAY *current_arr = prd_array_get_unsafe(&prd_stress_state.prd_array);
368
369 size_t current_size = current_arr ? current_arr->size : 0;
370 size_t new_size = current_size + 10;
371
372 if (new_size > 500)
373 new_size = 10;
374
375 PRD_ARRAY *new_arr = prd_array_create(new_size);
376
377 if (current_arr && current_size > 0) {
378 size_t copy_count = (current_size < new_size) ? current_size : new_size;
379 for(size_t i = 0; i < copy_count; i++) {
380 new_arr->entries[i].rd = current_arr->entries[i].rd;
381 new_arr->entries[i].id = current_arr->entries[i].id;
382 new_arr->entries[i].rda = NULL;
383 }
384 }
385
386 for (size_t i = (current_size < new_size ? current_size : 0); i < new_size; i++) {
387 new_arr->entries[i].rd = (void *)(uintptr_t)(i + 1);
388 new_arr->entries[i].id = "test";
389 }
390
391 PRD_ARRAY *old_arr = prd_array_replace(&prd_stress_state.prd_array, new_arr);
392
393 if (old_arr)
394 prd_array_release(old_arr);
395
396 __atomic_fetch_add(&prd_stress_state.grow_count, 1, __ATOMIC_RELAXED);
397
398 tinysleep();
399 }
400
401 // Clear collector_tid (like pluginsd_set_scope_chart does when switching away)
402 __atomic_store_n(&prd_stress_state.collector_tid, 0, __ATOMIC_RELEASE);
403
404 // Signal that writer phase is complete
405 __atomic_store_n(&prd_stress_state.writer_is_running, false, __ATOMIC_RELEASE);
406
407 // Wait until controller signals us to run again
408 while (__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE) &&
409 !__atomic_load_n(&prd_stress_state.writer_should_run, __ATOMIC_ACQUIRE)) {
410 tinysleep();
411 }
412 }
413 }
414
415 static void prd_stress_cleanup_thread(void *arg __maybe_unused) {
416 while (__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE)) {
417
418 // Wait until writer is fully stopped (simulates stream_receiver_signal_to_stop_and_wait)
419 while (__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE) &&
420 __atomic_load_n(&prd_stress_state.writer_is_running, __ATOMIC_ACQUIRE)) {
421 tinysleep();
422 }
423
424 if (!__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE))
425 break;
426
427 // Now safe to cleanup - writer is fully stopped
428 spinlock_lock(&prd_stress_state.spinlock);
429
430 // Double-check collector_tid (should be 0 since writer stopped)
431 pid_t collector_tid = __atomic_load_n(&prd_stress_state.collector_tid, __ATOMIC_ACQUIRE);
432 if (collector_tid != 0) {
433 // This shouldn't happen if lifecycle is correct
434 spinlock_unlock(&prd_stress_state.spinlock);
435 continue;
436 }
437
438 PRD_ARRAY *old_arr = prd_array_replace(&prd_stress_state.prd_array, NULL);
439
440 spinlock_unlock(&prd_stress_state.spinlock);
441
442 if (old_arr) {
443 for (size_t i = 0; i < old_arr->size; i++) {
444 old_arr->entries[i].rda = NULL;
445 old_arr->entries[i].rd = NULL;
446 old_arr->entries[i].id = NULL;
447 }
448
449 prd_array_release(old_arr);
450 __atomic_fetch_add(&prd_stress_state.cleanup_count, 1, __ATOMIC_RELAXED);
451 }
452
453 tinysleep();
454 }
455 }
456
457 // Controller thread - orchestrates the lifecycle phases
458 static void prd_stress_controller_thread(void *arg __maybe_unused) {
459 while (__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE)) {
460
461 // Start writer phase
462 __atomic_store_n(&prd_stress_state.writer_should_run, true, __ATOMIC_RELEASE);
463
464 // Wait for writer to start and run
465 sleep_usec(10000); // 10ms - let writer run
466
467 // Signal writer to stop (simulates stream receiver stopping)
468 __atomic_store_n(&prd_stress_state.writer_should_run, false, __ATOMIC_RELEASE);
469
470 // Wait for writer to fully stop
471 while (__atomic_load_n(&prd_stress_state.test_running, __ATOMIC_ACQUIRE) &&
472 __atomic_load_n(&prd_stress_state.writer_is_running, __ATOMIC_ACQUIRE)) {
473 tinysleep();
474 }
475
476 // Cleanup phase - cleaner will run now that writer is stopped
477 sleep_usec(5000); // 5ms - let cleanup run
478
479 __atomic_fetch_add(&prd_stress_state.phase_count, 1, __ATOMIC_RELAXED);
480 }
481 }
482
483 int prd_array_stress_test(void) {
484 int duration_secs = PRD_STRESS_TEST_DURATION_SEC;
485
486 fprintf(stderr, "\nPRD_ARRAY Lifecycle Stress Test\n");
487 fprintf(stderr, "================================\n");
488 fprintf(stderr, "Duration: %d seconds\n", duration_secs);
489 fprintf(stderr, "This test simulates production lifecycle:\n");
490 fprintf(stderr, " 1. Writer (collector) runs with collector_tid set\n");
491 fprintf(stderr, " 2. Writer fully stops (collector_tid cleared)\n");
492 fprintf(stderr, " 3. Cleaner runs cleanup\n");
493 fprintf(stderr, " 4. Repeat\n\n");
494
495 // Initialize state
496 memset(&prd_stress_state, 0, sizeof(prd_stress_state));
497 prd_stress_state.prd_array = prd_array_create(10);
498 spinlock_init(&prd_stress_state.spinlock);
499 __atomic_store_n(&prd_stress_state.test_running, true, __ATOMIC_RELEASE);
500
501 // Start threads
502 char thread_name[32];
503
504 snprintfz(thread_name, sizeof(thread_name), "PRDSTRESS_W");
505 ND_THREAD *writer_thread = nd_thread_create(thread_name, NETDATA_THREAD_OPTION_DEFAULT,
506 prd_stress_writer_thread, NULL);
507
508 snprintfz(thread_name, sizeof(thread_name), "PRDSTRESS_C");
509 ND_THREAD *cleanup_thread = nd_thread_create(thread_name, NETDATA_THREAD_OPTION_DEFAULT,
510 prd_stress_cleanup_thread, NULL);
511
512 snprintfz(thread_name, sizeof(thread_name), "PRDSTRESS_CTRL");
513 ND_THREAD *controller_thread = nd_thread_create(thread_name, NETDATA_THREAD_OPTION_DEFAULT,
514 prd_stress_controller_thread, NULL);
515
516 // Run the test
517 fprintf(stderr, "Running stress test...\n");
518 for (int i = 0; i < duration_secs; i++) {
519 sleep_usec(USEC_PER_SEC);
520 fprintf(stderr, " %d/%d sec - phases: %"PRIu64", grows: %"PRIu64", cleanups: %"PRIu64"\n",
521 i + 1, duration_secs,
522 __atomic_load_n(&prd_stress_state.phase_count, __ATOMIC_RELAXED),
523 __atomic_load_n(&prd_stress_state.grow_count, __ATOMIC_RELAXED),
524 __atomic_load_n(&prd_stress_state.cleanup_count, __ATOMIC_RELAXED));
525 }
526
527 // Stop all threads
528 __atomic_store_n(&prd_stress_state.test_running, false, __ATOMIC_RELEASE);
529 __atomic_store_n(&prd_stress_state.writer_should_run, true, __ATOMIC_RELEASE); // Unblock writer
530
531 nd_thread_join(controller_thread);
532 nd_thread_join(writer_thread);
533 nd_thread_join(cleanup_thread);
534
535 // Final cleanup
536 PRD_ARRAY *final_arr = prd_array_replace(&prd_stress_state.prd_array, NULL);
537 if (final_arr)
538 prd_array_release(final_arr);
539
540 // Print results
541 fprintf(stderr, "\nTest completed!\n");
542 fprintf(stderr, "===============\n");
543 fprintf(stderr, "Total phases: %"PRIu64"\n", prd_stress_state.phase_count);
544 fprintf(stderr, "Total grows: %"PRIu64"\n", prd_stress_state.grow_count);
545 fprintf(stderr, "Total cleanups: %"PRIu64"\n", prd_stress_state.cleanup_count);
546
547 if (prd_stress_state.cleanup_count > 0 && prd_stress_state.grow_count > 0) {
548 fprintf(stderr, "\nSUCCESS: Lifecycle separation validated\n");
549 fprintf(stderr, "- Writer and cleaner ran in non-overlapping phases\n");
550 fprintf(stderr, "- No concurrent access to the array\n");
551 fprintf(stderr, "- Reference counting worked correctly\n");
552 return 0;
553 } else {
554 fprintf(stderr, "\nWARNING: Low activity - increase test duration\n");
555 return 1;
556 }
557 }