master
cc 673 lines 22 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "ml_private.h"
4
5 #include "database/sqlite/sqlite_db_migration.h"
6
7 #include <random>
8
9 #define ML_METADATA_VERSION 2
10
11 static void ml_host_clear_context_anomaly_rate(ml_host_t *host)
12 {
13 spinlock_lock(&host->context_anomaly_rate_spinlock);
14
15 for (auto &entry : host->context_anomaly_rate)
16 string_freez(entry.first);
17
18 host->context_anomaly_rate.clear();
19
20 spinlock_unlock(&host->context_anomaly_rate_spinlock);
21 }
22
23 static void ml_dimension_enqueue_create_model(RRDHOST *rh, RRDDIM *rd)
24 {
25 ml_host_t *host = (ml_host_t *) __atomic_load_n(&rh->ml_host, __ATOMIC_ACQUIRE);
26 if (!host)
27 return;
28
29 ml_dimension_t *dim = (ml_dimension_t *) rd->ml_dimension;
30 if (!dim)
31 return;
32
33 spinlock_lock(&dim->slock);
34 bool should_enqueue = !dim->create_new_model_queued &&
35 dim->ts == TRAINING_STATUS_UNTRAINED &&
36 (!dim->has_received_downstream_model || dim->km_contexts.empty());
37 if (should_enqueue)
38 dim->create_new_model_queued = true;
39 spinlock_unlock(&dim->slock);
40
41 if (!should_enqueue)
42 return;
43
44 ml_queue_item_t item;
45 item.type = ML_QUEUE_ITEM_TYPE_CREATE_NEW_MODEL;
46 item.create_new_model.DLI = DimensionLookupInfo(
47 &rh->machine_guid[0],
48 rd->rrdset->id,
49 rd->id
50 );
51
52 ml_queue_push(host->queue, item);
53 }
54
55 bool ml_capable()
56 {
57 return true;
58 }
59
60 bool ml_enabled(RRDHOST *rh)
61 {
62 if (!rh)
63 return false;
64
65 if (!Cfg.enable_anomaly_detection)
66 return false;
67
68 if (simple_pattern_matches(Cfg.sp_host_to_skip, rrdhost_hostname(rh)))
69 return false;
70
71 return true;
72 }
73
74 bool ml_streaming_enabled()
75 {
76 return Cfg.stream_anomaly_detection_charts;
77 }
78
79 void ml_host_new(RRDHOST *rh)
80 {
81 if (!ml_enabled(rh))
82 return;
83
84 ml_host_t *host = new ml_host_t();
85
86 host->rh = rh;
87 host->mls = ml_machine_learning_stats_t();
88 host->host_anomaly_rate = 0.0;
89 host->anomaly_rate_rs = NULL;
90
91 static std::atomic<size_t> times_called(0);
92 host->queue = Cfg.workers[times_called++ % Cfg.num_worker_threads].queue;
93
94 netdata_mutex_init(&host->mutex);
95 netdata_mutex_init(&host->start_stop_mutex);
96 spinlock_init(&host->context_anomaly_rate_spinlock);
97
98 host->ml_running = false;
99 host->ml_stop_generation = 0;
100
101 // Publish with release semantics so readers that load rh->ml_host with
102 // acquire semantics observe the host's `rh`, `ml_running`, `mutex`,
103 // `queue`, etc. as fully initialized. Without this, the C++ compiler
104 // may reorder field stores after the publish store of rh->ml_host, and
105 // a concurrent reader would see host != NULL with partially-initialized
106 // fields, producing SIGSEGV faults inside ml_dimension_is_anomalous and
107 // similar readers.
108 __atomic_store_n(&rh->ml_host, (rrd_ml_host_t *)host, __ATOMIC_RELEASE);
109 }
110
111 void ml_host_delete(RRDHOST *rh)
112 {
113 // Atomically detach `rh->ml_host` and obtain the previous pointer in a
114 // single RMW. Using exchange (rather than separate load + store) keeps
115 // the unpublish and the freeing on this thread strictly ordered: no
116 // store/operation that follows can be reordered before the unpublish,
117 // so concurrent readers observe either the live host or NULL -- never
118 // the freed host memory.
119 ml_host_t *host = (ml_host_t *) __atomic_exchange_n(&rh->ml_host, (rrd_ml_host_t *)NULL, __ATOMIC_ACQ_REL);
120 if (!host)
121 return;
122
123 ml_host_clear_context_anomaly_rate(host);
124 netdata_mutex_destroy(&host->mutex);
125 netdata_mutex_destroy(&host->start_stop_mutex);
126
127 delete host;
128 }
129
130 void ml_host_start(RRDHOST *rh) {
131 ml_host_t *host = (ml_host_t *) __atomic_load_n(&rh->ml_host, __ATOMIC_ACQUIRE);
132 if (!host)
133 return;
134
135 // Serialize against ml_host_stop(): we must not re-enable ml_running
136 // while a stop is still resetting chart/dim state (see ml_host_stop),
137 // and concurrent ml_host_start() calls must not run the sweep twice.
138 netdata_mutex_lock(&host->start_stop_mutex);
139
140 if (host->ml_running) {
141 netdata_mutex_unlock(&host->start_stop_mutex);
142 return;
143 }
144
145 // Run the sweep under host->mutex so the visibility window of the flag
146 // flip is bounded by the same critical section that performs the sweep.
147 netdata_mutex_lock(&host->mutex);
148
149 host->ml_running = true;
150
151 void *rsp = NULL;
152 rrdset_foreach_read(rsp, host->rh) {
153 RRDSET *rs = static_cast<RRDSET *>(rsp);
154
155 void *rdp = NULL;
156 rrddim_foreach_read(rdp, rs) {
157 RRDDIM *rd = static_cast<RRDDIM *>(rdp);
158 ml_dimension_enqueue_create_model(rh, rd);
159 }
160 rrddim_foreach_done(rdp);
161 }
162 rrdset_foreach_done(rsp);
163
164 netdata_mutex_unlock(&host->mutex);
165 netdata_mutex_unlock(&host->start_stop_mutex);
166 }
167
168 void ml_host_stop(RRDHOST *rh) {
169 ml_host_t *host = (ml_host_t *) __atomic_load_n(&rh->ml_host, __ATOMIC_ACQUIRE);
170 if (!host)
171 return;
172
173 // Serialize with ml_host_start() for the WHOLE stop sequence, including
174 // the unlocked chart/dim reset walk and the final generation bump. If a
175 // racing start could flip ml_running back to true mid-reset, a concurrent
176 // ml_host_detect_once would observe ml_running==true with an unchanged
177 // stop generation and publish a snapshot torn by our in-flight resets.
178 netdata_mutex_lock(&host->start_stop_mutex);
179
180 if (!host->ml_running) {
181 netdata_mutex_unlock(&host->start_stop_mutex);
182 return;
183 }
184
185 // Prevent new ML activity from publishing while we reset host/dimension
186 // state. The ml_running flag gates collectors and the detect loop; the
187 // stop generation is bumped at the end of the function so a concurrent
188 // ml_host_detect_once that observes the new generation is guaranteed to
189 // also see all of our chart->mls / dim resets via seq_cst ordering.
190 host->ml_running = false;
191
192 netdata_mutex_lock(&host->mutex);
193
194 // reset host stats
195 host->mls = ml_machine_learning_stats_t();
196 ml_host_clear_context_anomaly_rate(host);
197
198 // Chart deletion can hold the dictionary writer across lengthy cleanup.
199 // Do not carry host->mutex into the traversal below.
200 netdata_mutex_unlock(&host->mutex);
201
202 // reset charts/dims
203 void *rsp = NULL;
204 rrdset_foreach_read(rsp, host->rh) {
205 RRDSET *rs = static_cast<RRDSET *>(rsp);
206
207 ml_chart_t *chart = (ml_chart_t *) __atomic_load_n(&rs->ml_chart, __ATOMIC_ACQUIRE);
208 if (!chart)
209 continue;
210
211 // reset chart
212 chart->mls = ml_machine_learning_stats_t();
213
214 void *rdp = NULL;
215 rrddim_foreach_read(rdp, rs) {
216 RRDDIM *rd = static_cast<RRDDIM *>(rdp);
217
218 ml_dimension_t *dim = (ml_dimension_t *) rd->ml_dimension;
219 if (!dim)
220 continue;
221
222 spinlock_lock(&dim->slock);
223
224 dim->mt = METRIC_TYPE_CONSTANT;
225 dim->ts = TRAINING_STATUS_UNTRAINED;
226
227 dim->suppression_anomaly_counter = 0;
228 dim->suppression_window_counter = 0;
229 dim->cns.clear();
230 dim->cns_head = 0;
231 dim->km_contexts.clear();
232 dim->has_received_downstream_model = false;
233 // create_new_model_queued not reset here: stop does not drain the
234 // worker queue, so pending CREATE_NEW_MODEL items remain valid.
235 dim->reset_generation++;
236
237 spinlock_unlock(&dim->slock);
238 }
239 rrddim_foreach_done(rdp);
240 }
241 rrdset_foreach_done(rsp);
242
243 // Publish the stop only after every chart->mls / dim reset is committed.
244 // ml_host_detect_once treats a generation change as "discard the snapshot",
245 // so bumping here guarantees that if detect saw stale chart->mls it will
246 // either also observe the new generation or have already published before
247 // any of our resets started.
248 host->ml_stop_generation.fetch_add(1);
249
250 netdata_mutex_unlock(&host->start_stop_mutex);
251 }
252
253 void ml_host_get_info(RRDHOST *rh, BUFFER *wb)
254 {
255 ml_host_t *host = (ml_host_t *) __atomic_load_n(&rh->ml_host, __ATOMIC_ACQUIRE);
256 if (!host) {
257 buffer_json_member_add_boolean(wb, "enabled", false);
258 return;
259 }
260
261 buffer_json_member_add_uint64(wb, "version", 1);
262
263 buffer_json_member_add_boolean(wb, "enabled", Cfg.enable_anomaly_detection);
264
265 buffer_json_member_add_uint64(wb, "training-window", Cfg.training_window);
266 buffer_json_member_add_uint64(wb, "min-training-window", Cfg.min_training_window);
267 buffer_json_member_add_uint64(wb, "max-training-vectors", Cfg.max_training_vectors);
268 buffer_json_member_add_uint64(wb, "max-samples-to-smooth", Cfg.max_samples_to_smooth);
269 buffer_json_member_add_uint64(wb, "train-every", Cfg.train_every);
270
271 buffer_json_member_add_uint64(wb, "diff-n", Cfg.diff_n);
272 buffer_json_member_add_uint64(wb, "lag-n", Cfg.lag_n);
273
274 buffer_json_member_add_uint64(wb, "max-kmeans-iters", Cfg.max_kmeans_iters);
275
276 buffer_json_member_add_double(wb, "dimension-anomaly-score-threshold", Cfg.dimension_anomaly_score_threshold);
277
278 buffer_json_member_add_string(wb, "anomaly-detection-grouping-method", time_grouping_id2txt(Cfg.anomaly_detection_grouping_method));
279
280 buffer_json_member_add_int64(wb, "anomaly-detection-query-duration", Cfg.anomaly_detection_query_duration);
281
282 buffer_json_member_add_string(wb, "hosts-to-skip", Cfg.hosts_to_skip.c_str());
283 buffer_json_member_add_string(wb, "charts-to-skip", Cfg.charts_to_skip.c_str());
284 }
285
286 void ml_host_get_detection_info(RRDHOST *rh, BUFFER *wb)
287 {
288 ml_host_t *host = (ml_host_t *) __atomic_load_n(&rh->ml_host, __ATOMIC_ACQUIRE);
289 if (!host)
290 return;
291
292 netdata_mutex_lock(&host->mutex);
293
294 buffer_json_member_add_uint64(wb, "version", 2);
295 buffer_json_member_add_uint64(wb, "ml-running", host->ml_running);
296 buffer_json_member_add_uint64(wb, "anomalous-dimensions", host->mls.num_anomalous_dimensions);
297 buffer_json_member_add_uint64(wb, "normal-dimensions", host->mls.num_normal_dimensions);
298 buffer_json_member_add_uint64(wb, "total-dimensions", host->mls.num_anomalous_dimensions +
299 host->mls.num_normal_dimensions);
300 buffer_json_member_add_uint64(wb, "trained-dimensions", host->mls.num_training_status_trained +
301 host->mls.num_training_status_pending_with_model);
302 netdata_mutex_unlock(&host->mutex);
303 }
304
305 bool ml_host_get_host_status(RRDHOST *rh, struct ml_metrics_statistics *mlm) {
306 ml_host_t *host = (ml_host_t *) __atomic_load_n(&rh->ml_host, __ATOMIC_ACQUIRE);
307 if (!host) {
308 memset(mlm, 0, sizeof(*mlm));
309 return false;
310 }
311
312 netdata_mutex_lock(&host->mutex);
313
314 mlm->anomalous = host->mls.num_anomalous_dimensions;
315 mlm->normal = host->mls.num_normal_dimensions;
316 mlm->trained = host->mls.num_training_status_trained + host->mls.num_training_status_pending_with_model;
317 mlm->pending = host->mls.num_training_status_untrained + host->mls.num_training_status_pending_without_model;
318 mlm->silenced = host->mls.num_training_status_silenced;
319
320 netdata_mutex_unlock(&host->mutex);
321
322 return true;
323 }
324
325 bool ml_host_running(RRDHOST *rh) {
326 ml_host_t *host = (ml_host_t *) __atomic_load_n(&rh->ml_host, __ATOMIC_ACQUIRE);
327 if(!host)
328 return false;
329
330 return host->ml_running;
331 }
332
333 void ml_host_get_models(RRDHOST *rh, BUFFER *wb)
334 {
335 UNUSED(rh);
336 UNUSED(wb);
337
338 // TODO: To be implemented
339 netdata_log_error("Fetching KMeans models is not supported yet");
340 }
341
342 void ml_chart_new(RRDSET *rs)
343 {
344 ml_host_t *host = (ml_host_t *) __atomic_load_n(&rs->rrdhost->ml_host, __ATOMIC_ACQUIRE);
345 if (!host)
346 return;
347
348 ml_chart_t *chart = new ml_chart_t();
349
350 chart->rs = rs;
351 chart->mls = ml_machine_learning_stats_t();
352
353 // Publish with release semantics so readers that load rs->ml_chart with
354 // acquire semantics observe the chart's `rs` and `mls` fields as fully
355 // initialized. Without this, the C++ compiler may reorder the plain
356 // `chart->rs = rs` store after the publish store of rs->ml_chart, and a
357 // concurrent reader would see chart != NULL with chart->rs still NULL
358 // (from value-init in `new ml_chart_t()`), producing the SIGSEGV /
359 // MAPERR / 0x80 fault inside ml_chart_is_available_for_ml.
360 __atomic_store_n(&rs->ml_chart, (rrd_ml_chart_t *)chart, __ATOMIC_RELEASE);
361 }
362
363 void ml_chart_delete(RRDSET *rs)
364 {
365 ml_host_t *host = (ml_host_t *) __atomic_load_n(&rs->rrdhost->ml_host, __ATOMIC_ACQUIRE);
366 if (!host)
367 return;
368
369 // Atomically detach `rs->ml_chart` and obtain the previous pointer in a
370 // single RMW. Using exchange (rather than separate load + store) keeps
371 // the unpublish and the freeing on this thread strictly ordered: no
372 // store/operation that follows can be reordered before the unpublish,
373 // so concurrent readers observe either the live chart (with chart->rs
374 // set) or NULL -- never the freed chart memory.
375 ml_chart_t *chart = (ml_chart_t *) __atomic_exchange_n(&rs->ml_chart, (rrd_ml_chart_t *)NULL, __ATOMIC_ACQ_REL);
376 delete chart;
377 }
378
379 ALWAYS_INLINE_ONLY bool ml_chart_update_begin(RRDSET *rs)
380 {
381 ml_chart_t *chart = (ml_chart_t *) __atomic_load_n(&rs->ml_chart, __ATOMIC_ACQUIRE);
382 if (!chart)
383 return false;
384
385 chart->mls = {};
386 return true;
387 }
388
389 void ml_chart_update_end(RRDSET *rs)
390 {
391 ml_chart_t *chart = (ml_chart_t *) __atomic_load_n(&rs->ml_chart, __ATOMIC_ACQUIRE);
392 if (!chart)
393 return;
394 }
395
396 void ml_dimension_new(RRDDIM *rd)
397 {
398 ml_chart_t *chart = (ml_chart_t *) __atomic_load_n(&rd->rrdset->ml_chart, __ATOMIC_ACQUIRE);
399 if (!chart)
400 return;
401
402 ml_dimension_t *dim = new ml_dimension_t();
403
404 dim->rd = rd;
405
406 dim->mt = METRIC_TYPE_CONSTANT;
407 dim->ts = TRAINING_STATUS_UNTRAINED;
408 dim->suppression_anomaly_counter = 0;
409 dim->suppression_window_counter = 0;
410 dim->training_in_progress = false;
411 dim->has_received_downstream_model = false;
412 dim->create_new_model_queued = false;
413 dim->reset_generation = 0;
414 dim->cns_head = 0;
415
416 ml_kmeans_init(&dim->kmeans);
417
418 if (simple_pattern_matches(Cfg.sp_charts_to_skip, rrdset_name(rd->rrdset)))
419 dim->mls = MACHINE_LEARNING_STATUS_DISABLED_DUE_TO_EXCLUDED_CHART;
420 else
421 dim->mls = MACHINE_LEARNING_STATUS_ENABLED;
422
423 spinlock_init(&dim->slock);
424
425 dim->km_contexts.reserve(Cfg.num_models_to_use);
426
427 rd->ml_dimension = (rrd_ml_dimension_t *) dim;
428
429 metaqueue_ml_load_models(rd);
430
431 // Only enqueue once ml is running for this host. Otherwise, ml_host_start()
432 // will sweep all untrained dimensions and enqueue them when it runs.
433 // This avoids double-enqueueing the same dim from both paths.
434 RRDHOST *rh = rd->rrdset->rrdhost;
435 ml_host_t *host = (ml_host_t *) __atomic_load_n(&rh->ml_host, __ATOMIC_ACQUIRE);
436 if (host && host->ml_running)
437 ml_dimension_enqueue_create_model(rh, rd);
438 }
439
440 void ml_dimension_delete(RRDDIM *rd)
441 {
442 ml_dimension_t *dim = (ml_dimension_t *) rd->ml_dimension;
443 if (!dim)
444 return;
445
446 // Wait for any in-progress training to complete before deleting
447 // This prevents use-after-free crashes when training thread accesses dim->rd
448 size_t wait_iterations = 0;
449 const size_t max_wait_iterations = 3000; // 30 seconds max (3000 * 10ms)
450
451 spinlock_lock(&dim->slock);
452 while (dim->training_in_progress && wait_iterations < max_wait_iterations) {
453 spinlock_unlock(&dim->slock);
454 sleep_usec(10000); // Wait 10ms
455 wait_iterations++;
456 spinlock_lock(&dim->slock);
457 }
458
459 if (dim->training_in_progress) {
460 // Training is stuck, but we can't wait forever
461 // Log the issue but proceed with deletion
462 netdata_log_error("ML: Dimension '%s' of chart '%s' is being deleted while training is in progress after waiting %zu ms",
463 rrddim_id(rd), rrdset_id(rd->rrdset), wait_iterations * 10);
464 }
465
466 spinlock_unlock(&dim->slock);
467
468 delete dim;
469 rd->ml_dimension = NULL;
470 }
471
472 ALWAYS_INLINE_ONLY void ml_dimension_received_anomaly(RRDDIM *rd, bool is_anomalous) {
473 ml_dimension_t *dim = (ml_dimension_t *) rd->ml_dimension;
474 if (!dim)
475 return;
476
477 ml_host_t *host = (ml_host_t *) __atomic_load_n(&rd->rrdset->rrdhost->ml_host, __ATOMIC_ACQUIRE);
478 if (!host || !host->ml_running)
479 return;
480
481 ml_chart_t *chart = (ml_chart_t *) __atomic_load_n(&rd->rrdset->ml_chart, __ATOMIC_ACQUIRE);
482 if (!chart)
483 return;
484
485 ml_chart_update_dimension(chart, dim, is_anomalous);
486 }
487
488 bool ml_dimension_is_anomalous(RRDDIM *rd, time_t curr_time, double value, bool exists)
489 {
490 UNUSED(curr_time);
491
492 ml_dimension_t *dim = (ml_dimension_t *) rd->ml_dimension;
493 if (!dim)
494 return false;
495
496 ml_host_t *host = (ml_host_t *) __atomic_load_n(&rd->rrdset->rrdhost->ml_host, __ATOMIC_ACQUIRE);
497 if (!host || !host->ml_running)
498 return false;
499
500 ml_chart_t *chart = (ml_chart_t *) __atomic_load_n(&rd->rrdset->ml_chart, __ATOMIC_ACQUIRE);
501 if (!chart)
502 return false;
503
504 bool is_anomalous = ml_dimension_predict(dim, value, exists);
505 ml_chart_update_dimension(chart, dim, is_anomalous);
506
507 return is_anomalous;
508 }
509
510 void ml_init()
511 {
512 // Read config values
513 ml_config_load(&Cfg);
514
515 if (!Cfg.enable_anomaly_detection)
516 return;
517
518 // Generate random numbers to efficiently sample the features we need
519 // for KMeans clustering.
520 std::random_device RD;
521 std::mt19937 Gen(RD());
522
523 Cfg.random_nums.reserve(Cfg.max_training_vectors);
524 for (size_t Idx = 0; Idx != Cfg.max_training_vectors; Idx++)
525 Cfg.random_nums.push_back(Gen());
526
527 // init training thread-specific data
528 Cfg.workers.resize(Cfg.num_worker_threads);
529 for (size_t idx = 0; idx != Cfg.num_worker_threads; idx++) {
530 ml_worker_t *worker = &Cfg.workers[idx];
531
532 // Calculate max elements needed based on the highest frequency metrics
533 // For 1-second metrics: training_window samples
534 // We allocate for worst case (1-second update frequency)
535 size_t max_elements_needed_for_training = (size_t) Cfg.training_window * (size_t) (Cfg.lag_n + 1);
536 worker->training_cns = new calculated_number_t[max_elements_needed_for_training]();
537 worker->scratch_training_cns = new calculated_number_t[max_elements_needed_for_training]();
538
539 worker->id = idx;
540 worker->queue = ml_queue_init();
541 worker->pending_model_info.reserve(Cfg.flush_models_batch_size);
542 netdata_mutex_init(&worker->nd_mutex);
543
544 // Initialize reusable buffers for streaming kmeans models
545 worker->stream_payload_buffer = buffer_create(0, NULL);
546 worker->stream_wb_buffer = buffer_create(0, NULL);
547 }
548
549 // open sqlite db
550 char path[FILENAME_MAX];
551 snprintfz(path, FILENAME_MAX - 1, "%s/%s", netdata_configured_cache_dir, "ml.db");
552 int rc = sqlite3_open(path, &ml_db);
553 if (rc != SQLITE_OK) {
554 error_report("Failed to initialize database at %s, due to \"%s\"", path, sqlite3_errstr(rc));
555 sqlite3_close(ml_db);
556 ml_db = NULL;
557 }
558
559 // create table
560 if (ml_db) {
561 int target_version = perform_ml_database_migration(ml_db, ML_METADATA_VERSION);
562 if (configure_sqlite_database(ml_db, target_version, "ml_config")) {
563 error_report("Failed to setup ML database");
564 sqlite3_close(ml_db);
565 ml_db = NULL;
566 }
567 else {
568 char *err = NULL;
569 int rc = sqlite3_exec(ml_db, db_models_create_table, NULL, NULL, &err);
570 if (rc != SQLITE_OK) {
571 error_report("Failed to create models table (%s, %s)", sqlite3_errstr(rc), err ? err : "");
572 sqlite3_close(ml_db);
573 sqlite3_free(err);
574 ml_db = NULL;
575 }
576 }
577 }
578 }
579
580 uint64_t sqlite_get_ml_space(void)
581 {
582 return sqlite_get_db_space(ml_db);
583 }
584
585 void ml_fini() {
586 if (!Cfg.enable_anomaly_detection || !ml_db)
587 return;
588
589 sql_close_database(ml_db, "ML");
590 ml_db = NULL;
591 }
592
593 void ml_start_threads() {
594 if (!Cfg.enable_anomaly_detection)
595 return;
596
597 // start detection & training threads
598 Cfg.detection_stop = false;
599 Cfg.training_stop = false;
600
601 char tag[NETDATA_THREAD_TAG_MAX + 1];
602
603 snprintfz(tag, NETDATA_THREAD_TAG_MAX, "%s", "PREDICT");
604 Cfg.detection_thread = nd_thread_create(tag, NETDATA_THREAD_OPTION_DEFAULT, ml_detect_main, NULL);
605
606 for (size_t idx = 0; idx != Cfg.num_worker_threads; idx++) {
607 ml_worker_t *worker = &Cfg.workers[idx];
608 snprintfz(tag, NETDATA_THREAD_TAG_MAX, "TRAIN[%zu]", worker->id);
609 worker->nd_thread = nd_thread_create(tag, NETDATA_THREAD_OPTION_DEFAULT, ml_train_main, worker);
610 }
611 }
612
613 void ml_stop_threads()
614 {
615 if (!Cfg.enable_anomaly_detection)
616 return;
617
618 Cfg.detection_stop = true;
619 Cfg.training_stop = true;
620
621 if (!Cfg.detection_thread)
622 return;
623
624 nd_thread_join(Cfg.detection_thread);
625 Cfg.detection_thread = 0;
626
627 // signal the worker queue of each thread
628 for (size_t idx = 0; idx != Cfg.num_worker_threads; idx++) {
629 ml_worker_t *worker = &Cfg.workers[idx];
630 ml_queue_signal(worker->queue);
631 }
632
633 // join worker threads
634 for (size_t idx = 0; idx != Cfg.num_worker_threads; idx++) {
635 ml_worker_t *worker = &Cfg.workers[idx];
636
637 nd_thread_join(worker->nd_thread);
638 }
639
640 // clear worker thread data
641 for (size_t idx = 0; idx != Cfg.num_worker_threads; idx++) {
642 ml_worker_t *worker = &Cfg.workers[idx];
643
644 delete[] worker->training_cns;
645 delete[] worker->scratch_training_cns;
646 ml_queue_destroy(worker->queue);
647 netdata_mutex_destroy(&worker->nd_mutex);
648
649 // Free reusable buffers
650 buffer_free(worker->stream_payload_buffer);
651 buffer_free(worker->stream_wb_buffer);
652 }
653 }
654
655 bool ml_model_received_from_child(RRDHOST *host, const char *json)
656 {
657 UNUSED(host);
658
659 bool ok = ml_dimension_deserialize_kmeans(json);
660 if (!ok) {
661 global_statistics_ml_models_deserialization_failures();
662 }
663
664 return ok;
665 }
666
667 void ml_host_disconnected(RRDHOST *rh) {
668 ml_host_t *host = (ml_host_t *) __atomic_load_n(&rh->ml_host, __ATOMIC_ACQUIRE);
669 if (!host)
670 return;
671
672 __atomic_store_n(&host->reset_pointers, true, __ATOMIC_RELAXED);
673 }