Common System 0.2.0
Common interfaces and patterns for system integration
Loading...
Searching...
No Matches
config_watcher.h
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
29#pragma once
30
31#include "config_loader.h"
32#include "unified_config.h"
33
35
36#include <atomic>
37#include <chrono>
38#include <deque>
39#include <filesystem>
40#include <functional>
41#include <memory>
42#include <mutex>
43#include <shared_mutex>
44#include <string>
45#include <thread>
46#include <vector>
47
48// Platform-specific includes
49#if defined(__linux__)
50#include <sys/eventfd.h>
51#include <sys/inotify.h>
52#include <unistd.h>
53#include <poll.h>
54#elif defined(__APPLE__) || defined(__FreeBSD__)
55#include <sys/event.h>
56#include <sys/time.h>
57#include <fcntl.h>
58#include <unistd.h>
59#elif defined(_WIN32)
60#ifndef WIN32_LEAN_AND_MEAN
61#define WIN32_LEAN_AND_MEAN
62#endif
63#include <windows.h>
64#endif
65
66namespace kcenon::common::config {
67
71namespace watcher_error_codes {
72 constexpr int watch_failed = 2001;
73 constexpr int reload_failed = 2002;
74 constexpr int validation_failed = 2003;
75 constexpr int rollback_failed = 2004;
76 constexpr int not_started = 2005;
77 constexpr int already_running = 2006;
78 constexpr int platform_not_supported = 2007;
79}
80
86 std::chrono::system_clock::time_point timestamp;
87
89 uint64_t version;
90
92 std::vector<std::string> changed_fields;
93
95 bool success;
96
98 std::string error_message;
99};
100
106 uint64_t version;
107
109 std::chrono::system_clock::time_point timestamp;
110
113};
114
144public:
146 using change_callback = std::function<void(const unified_config& old_config,
147 const unified_config& new_config)>;
148
150 using error_callback = std::function<void(const std::string& error_message)>;
151
157 explicit config_watcher(const std::string& config_path, size_t max_history = 10)
159 , version_(0)
160 , max_history_(max_history)
161 , running_(false)
162#if defined(__linux__)
163 , inotify_fd_(-1)
164 , watch_fd_(-1)
165 , shutdown_fd_(-1)
166#elif defined(__APPLE__) || defined(__FreeBSD__)
167 , kqueue_fd_(-1)
168 , file_fd_(-1)
169#elif defined(_WIN32)
170 , dir_handle_(INVALID_HANDLE_VALUE)
171#endif
172 {
173 // Load initial configuration
174 auto result = config_loader::load(config_path_);
175 if (result.is_ok()) {
176 current_config_ = result.value();
178 } else {
179 // Start with defaults if file doesn't exist or can't be loaded
182 }
183 }
184
189 stop();
190 }
191
192 // Non-copyable, non-movable
197
203 if (running_.load()) {
206 "Config watcher is already running",
207 "config_watcher"
208 );
209 }
210
211 auto init_result = init_platform_watcher();
212 if (init_result.is_err()) {
213 return init_result;
214 }
215
216 running_.store(true);
217 watch_thread_ = std::thread([this]() { watch_loop(); });
218
219 return VoidResult::ok({});
220 }
221
225 void stop() {
226 if (!running_.load()) {
227 return;
228 }
229
230 running_.store(false);
231
232 // Signal the watch thread to wake up
234
235 // Wait for the thread to exit before closing file descriptors
236 if (watch_thread_.joinable()) {
237 watch_thread_.join();
238 }
239
240 // Thread has exited; safe to close platform resources
242 }
243
248 bool is_running() const {
249 return running_.load();
250 }
251
260 void on_change(change_callback callback) {
261 std::lock_guard<std::mutex> lock(callbacks_mutex_);
262 change_callbacks_.push_back(std::move(callback));
263 }
264
272 void on_error(error_callback callback) {
273 std::lock_guard<std::mutex> lock(callbacks_mutex_);
274 error_callbacks_.push_back(std::move(callback));
275 }
276
282 return do_reload();
283 }
284
289 const unified_config& current() const {
290 std::shared_lock<std::shared_mutex> lock(config_mutex_);
291 return current_config_;
292 }
293
298 uint64_t version() const {
299 return version_.load();
300 }
301
307 std::vector<config_snapshot> history(size_t count = 0) const {
308 std::lock_guard<std::mutex> lock(history_mutex_);
309
310 if (count == 0 || count > history_.size()) {
311 return {history_.rbegin(), history_.rend()};
312 }
313
314 std::vector<config_snapshot> result;
315 result.reserve(count);
316 auto it = history_.rbegin();
317 for (size_t i = 0; i < count && it != history_.rend(); ++i, ++it) {
318 result.push_back(*it);
319 }
320 return result;
321 }
322
328 VoidResult rollback(uint64_t target_version) {
329 std::lock_guard<std::mutex> lock(history_mutex_);
330
331 for (const auto& snapshot : history_) {
332 if (snapshot.version == target_version) {
333 std::unique_lock<std::shared_mutex> config_lock(config_mutex_);
334 unified_config old_config = current_config_;
335 current_config_ = snapshot.config;
336 version_.fetch_add(1);
337
338 // Notify callbacks
339 config_lock.unlock();
340 notify_change(old_config, current_config_);
341
342 return VoidResult::ok({});
343 }
344 }
345
348 "Target version not found in history: " + std::to_string(target_version),
349 "config_watcher"
350 );
351 }
352
357 const std::string& config_path() const {
358 return config_path_;
359 }
360
366 std::vector<config_change_event> recent_events(size_t count = 10) const {
367 std::lock_guard<std::mutex> lock(events_mutex_);
368
369 if (count == 0 || count > events_.size()) {
370 return {events_.rbegin(), events_.rend()};
371 }
372
373 std::vector<config_change_event> result;
374 result.reserve(count);
375 auto it = events_.rbegin();
376 for (size_t i = 0; i < count && it != events_.rend(); ++i, ++it) {
377 result.push_back(*it);
378 }
379 return result;
380 }
381
382private:
387#if defined(__linux__)
388 return init_inotify();
389#elif defined(__APPLE__) || defined(__FreeBSD__)
390 return init_kqueue();
391#elif defined(_WIN32)
392 return init_win32_watcher();
393#else
396 "File watching not supported on this platform",
397 "config_watcher"
398 );
399#endif
400 }
401
406#if defined(__linux__)
407 signal_eventfd();
408#elif defined(__APPLE__) || defined(__FreeBSD__)
409 // Closing kqueue wakes up kevent() with an error
410 int kq = kqueue_fd_.exchange(-1);
411 if (kq >= 0) {
412 close(kq);
413 }
414#elif defined(_WIN32)
415 if (dir_handle_ != INVALID_HANDLE_VALUE) {
416 CancelIo(dir_handle_);
417 }
418#endif
419 }
420
425#if defined(__linux__)
426 cleanup_inotify();
427#elif defined(__APPLE__) || defined(__FreeBSD__)
428 cleanup_kqueue();
429#elif defined(_WIN32)
430 cleanup_win32_watcher();
431#endif
432 }
433
434#if defined(__linux__)
438 VoidResult init_inotify() {
439 shutdown_fd_ = eventfd(0, EFD_NONBLOCK);
440 if (shutdown_fd_ < 0) {
443 "Failed to create eventfd: " + std::string(strerror(errno)),
444 "config_watcher"
445 );
446 }
447
448 int ifd = inotify_init1(IN_NONBLOCK);
449 if (ifd < 0) {
450 close(shutdown_fd_);
451 shutdown_fd_ = -1;
454 "Failed to initialize inotify: " + std::string(strerror(errno)),
455 "config_watcher"
456 );
457 }
458
459 // Watch the parent directory (to handle file recreation)
460 std::filesystem::path path(config_path_);
461 std::string dir_path = path.parent_path().string();
462 if (dir_path.empty()) {
463 dir_path = ".";
464 }
465
466 int wfd = inotify_add_watch(ifd, dir_path.c_str(),
467 IN_MODIFY | IN_CREATE | IN_MOVED_TO | IN_CLOSE_WRITE);
468 if (wfd < 0) {
469 close(ifd);
470 close(shutdown_fd_);
471 shutdown_fd_ = -1;
474 "Failed to add inotify watch: " + std::string(strerror(errno)),
475 "config_watcher"
476 );
477 }
478
479 inotify_fd_.store(ifd);
480 watch_fd_.store(wfd);
481 return VoidResult::ok({});
482 }
483
484 void signal_eventfd() {
485 if (shutdown_fd_ >= 0) {
486 uint64_t val = 1;
487 [[maybe_unused]] auto unused = write(shutdown_fd_, &val, sizeof(val));
488 }
489 }
490
491 void cleanup_inotify() {
492 int wfd = watch_fd_.exchange(-1);
493 int ifd = inotify_fd_.exchange(-1);
494 if (wfd >= 0 && ifd >= 0) {
495 inotify_rm_watch(ifd, wfd);
496 }
497 if (ifd >= 0) {
498 close(ifd);
499 }
500 if (shutdown_fd_ >= 0) {
501 close(shutdown_fd_);
502 shutdown_fd_ = -1;
503 }
504 }
505
506 void watch_loop_linux() {
507 std::filesystem::path path(config_path_);
508 std::string filename = path.filename().string();
509
510 constexpr size_t EVENT_BUF_LEN = 4096;
511 alignas(struct inotify_event) char buffer[EVENT_BUF_LEN];
512
513 while (running_.load()) {
514 int ifd = inotify_fd_.load();
515 if (ifd < 0) break;
516
517 struct pollfd pfds[2] = {
518 {ifd, POLLIN, 0},
519 {shutdown_fd_, POLLIN, 0}
520 };
521 int ret = poll(pfds, 2, 500); // 500ms timeout
522
523 if (ret < 0) {
524 if (errno == EINTR) continue;
525 break;
526 }
527
528 if (ret == 0) continue; // Timeout
529
530 // Check shutdown signal first
531 if (pfds[1].revents & POLLIN) break;
532
533 if (!(pfds[0].revents & POLLIN)) continue;
534
535 ssize_t len = read(ifd, buffer, EVENT_BUF_LEN);
536 if (len < 0) {
537 if (errno == EAGAIN || errno == EINTR) continue;
538 break;
539 }
540
541 bool should_reload = false;
542 for (char* ptr = buffer; ptr < buffer + len; ) {
543 auto* event = reinterpret_cast<struct inotify_event*>(ptr);
544
545 if (event->len > 0) {
546 std::string event_name(event->name);
547 if (event_name == filename) {
548 if (event->mask & (IN_MODIFY | IN_CREATE | IN_MOVED_TO | IN_CLOSE_WRITE)) {
549 should_reload = true;
550 }
551 }
552 }
553
554 ptr += sizeof(struct inotify_event) + event->len;
555 }
556
557 if (should_reload) {
558 // Small delay to ensure file write is complete
559 std::this_thread::sleep_for(std::chrono::milliseconds(100));
560 do_reload();
561 }
562 }
563 }
564#endif
565
566#if defined(__APPLE__) || defined(__FreeBSD__)
570 VoidResult init_kqueue() {
571 kqueue_fd_ = kqueue();
572 if (kqueue_fd_ < 0) {
575 "Failed to create kqueue: " + std::string(strerror(errno)),
576 "config_watcher"
577 );
578 }
579
580 file_fd_ = open(config_path_.c_str(), O_RDONLY);
581 if (file_fd_ < 0) {
582 // If file doesn't exist, watch the directory instead
583 std::filesystem::path path(config_path_);
584 std::string dir_path = path.parent_path().string();
585 if (dir_path.empty()) {
586 dir_path = ".";
587 }
588
589 file_fd_ = open(dir_path.c_str(), O_RDONLY);
590 if (file_fd_ < 0) {
591 close(kqueue_fd_);
592 kqueue_fd_ = -1;
595 "Failed to open file/directory for watching: " + std::string(strerror(errno)),
596 "config_watcher"
597 );
598 }
599 watching_directory_ = true;
600 }
601
602 struct kevent change;
603 EV_SET(&change, file_fd_, EVFILT_VNODE,
604 EV_ADD | EV_ENABLE | EV_CLEAR,
605 NOTE_WRITE | NOTE_EXTEND | NOTE_RENAME | NOTE_DELETE | NOTE_ATTRIB,
606 0, nullptr);
607
608 if (kevent(kqueue_fd_, &change, 1, nullptr, 0, nullptr) < 0) {
609 close(file_fd_);
610 close(kqueue_fd_);
611 file_fd_ = -1;
612 kqueue_fd_ = -1;
615 "Failed to register kevent: " + std::string(strerror(errno)),
616 "config_watcher"
617 );
618 }
619
620 return VoidResult::ok({});
621 }
622
623 void cleanup_kqueue() {
624 // Close kqueue fd if not already closed by signal_watcher_shutdown()
625 int kq = kqueue_fd_.exchange(-1);
626 if (kq >= 0) {
627 close(kq);
628 }
629 // file_fd_ is cleaned up by watch_loop_kqueue() on exit
630 }
631
632 void watch_loop_kqueue() {
633 struct kevent event;
634 struct timespec timeout = {0, 500000000}; // 500ms
635
636 while (running_.load()) {
637 int kq = kqueue_fd_.load();
638 if (kq < 0) break;
639
640 int n = kevent(kq, nullptr, 0, &event, 1, &timeout);
641
642 if (n < 0) {
643 if (errno == EINTR) continue;
644 break;
645 }
646
647 if (n == 0) continue; // Timeout
648
649 if (event.fflags & (NOTE_WRITE | NOTE_EXTEND | NOTE_RENAME | NOTE_ATTRIB)) {
650 // Small delay to ensure file write is complete
651 std::this_thread::sleep_for(std::chrono::milliseconds(100));
652
653 // If file was deleted/renamed and we need to re-watch
654 if (event.fflags & (NOTE_DELETE | NOTE_RENAME)) {
655 // Try to reopen the file
656 std::lock_guard<std::mutex> lock(file_fd_mutex_);
657 if (file_fd_ >= 0) {
658 close(file_fd_);
659 }
660 file_fd_ = open(config_path_.c_str(), O_RDONLY);
661 if (file_fd_ >= 0) {
662 int kq_local = kqueue_fd_.load();
663 if (kq_local >= 0) {
664 struct kevent change;
665 EV_SET(&change, file_fd_, EVFILT_VNODE,
666 EV_ADD | EV_ENABLE | EV_CLEAR,
667 NOTE_WRITE | NOTE_EXTEND | NOTE_RENAME | NOTE_DELETE | NOTE_ATTRIB,
668 0, nullptr);
669 kevent(kq_local, &change, 1, nullptr, 0, nullptr);
670 }
671 }
672 }
673
674 do_reload();
675 }
676 }
677
678 // Cleanup file_fd_ when exiting the loop
679 std::lock_guard<std::mutex> lock(file_fd_mutex_);
680 if (file_fd_ >= 0) {
681 close(file_fd_);
682 file_fd_ = -1;
683 }
684 }
685
686 bool watching_directory_ = false;
687#endif
688
689#if defined(_WIN32)
693 VoidResult init_win32_watcher() {
694 std::filesystem::path path(config_path_);
695 std::wstring dir_path = path.parent_path().wstring();
696 if (dir_path.empty()) {
697 dir_path = L".";
698 }
699
700 dir_handle_ = CreateFileW(
701 dir_path.c_str(),
702 FILE_LIST_DIRECTORY,
703 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
704 nullptr,
705 OPEN_EXISTING,
706 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
707 nullptr
708 );
709
710 if (dir_handle_ == INVALID_HANDLE_VALUE) {
713 "Failed to open directory for watching",
714 "config_watcher"
715 );
716 }
717
718 return VoidResult::ok({});
719 }
720
721 void cleanup_win32_watcher() {
722 if (dir_handle_ != INVALID_HANDLE_VALUE) {
723 CancelIo(dir_handle_);
724 CloseHandle(dir_handle_);
725 dir_handle_ = INVALID_HANDLE_VALUE;
726 }
727 }
728
729 void watch_loop_win32() {
730 std::filesystem::path path(config_path_);
731 std::wstring filename = path.filename().wstring();
732
733 constexpr DWORD BUFFER_SIZE = 4096;
734 alignas(DWORD) char buffer[BUFFER_SIZE];
735
736 OVERLAPPED overlapped = {};
737 overlapped.hEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
738
739 while (running_.load()) {
740 DWORD bytes_returned = 0;
741 BOOL success = ReadDirectoryChangesW(
742 dir_handle_,
743 buffer,
744 BUFFER_SIZE,
745 FALSE,
746 FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_SIZE,
747 nullptr,
748 &overlapped,
749 nullptr
750 );
751
752 if (!success) {
753 break;
754 }
755
756 DWORD wait_result = WaitForSingleObject(overlapped.hEvent, 500);
757
758 if (wait_result == WAIT_TIMEOUT) {
759 CancelIo(dir_handle_);
760 continue;
761 }
762
763 if (wait_result != WAIT_OBJECT_0) {
764 break;
765 }
766
767 if (!GetOverlappedResult(dir_handle_, &overlapped, &bytes_returned, FALSE)) {
768 break;
769 }
770
771 ResetEvent(overlapped.hEvent);
772
773 bool should_reload = false;
774 auto* notification = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(buffer);
775
776 while (true) {
777 std::wstring changed_name(notification->FileName,
778 notification->FileNameLength / sizeof(WCHAR));
779
780 if (changed_name == filename) {
781 should_reload = true;
782 }
783
784 if (notification->NextEntryOffset == 0) break;
785 notification = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(
786 reinterpret_cast<char*>(notification) + notification->NextEntryOffset);
787 }
788
789 if (should_reload) {
790 std::this_thread::sleep_for(std::chrono::milliseconds(100));
791 do_reload();
792 }
793 }
794
795 CloseHandle(overlapped.hEvent);
796 }
797#endif
798
802 void watch_loop() {
803#if defined(__linux__)
804 watch_loop_linux();
805#elif defined(__APPLE__) || defined(__FreeBSD__)
806 watch_loop_kqueue();
807#elif defined(_WIN32)
808 watch_loop_win32();
809#endif
810 }
811
816 auto result = config_loader::load(config_path_);
817
819 event.timestamp = std::chrono::system_clock::now();
820 event.version = version_.load() + 1;
821
822 if (result.is_err()) {
823 event.success = false;
824 event.error_message = result.error().message;
825 add_event(event);
826 notify_error(result.error().message);
827 return make_error<std::monostate>(result.error());
828 }
829
830 unified_config new_config = result.value();
831
832 // Validate the new configuration
833 auto validation_result = config_loader::validate(new_config);
834 if (validation_result.is_err()) {
835 event.success = false;
836 event.error_message = validation_result.error().message;
837 add_event(event);
838 notify_error("Validation failed: " + validation_result.error().message);
841 "Configuration validation failed: " + validation_result.error().message,
842 "config_watcher"
843 );
844 }
845
846 // Get changed fields
847 std::unique_lock<std::shared_mutex> lock(config_mutex_);
848 event.changed_fields = get_changed_fields(current_config_, new_config);
849
850 // Check if any non-hot-reloadable fields changed
851 std::vector<std::string> non_reloadable_changes;
852 for (const auto& field : event.changed_fields) {
853 if (!is_hot_reloadable(field)) {
854 non_reloadable_changes.push_back(field);
855 }
856 }
857
858 // Update configuration
859 unified_config old_config = current_config_;
860 current_config_ = new_config;
861 version_.fetch_add(1);
862 event.version = version_.load();
863 event.success = true;
864
865 // Add to history
866 add_to_history(new_config);
867
868 lock.unlock();
869
870 // Record event
871 add_event(event);
872
873 // Notify callbacks
874 notify_change(old_config, new_config);
875
876 return VoidResult::ok({});
877 }
878
882 static std::vector<std::string> get_changed_fields(
883 const unified_config& old_cfg,
884 const unified_config& new_cfg
885 ) {
886 std::vector<std::string> changes;
887
888 // Thread config
889 if (old_cfg.thread.pool_size != new_cfg.thread.pool_size) {
890 changes.push_back("thread.pool_size");
891 }
892 if (old_cfg.thread.queue_type != new_cfg.thread.queue_type) {
893 changes.push_back("thread.queue_type");
894 }
895 if (old_cfg.thread.max_queue_size != new_cfg.thread.max_queue_size) {
896 changes.push_back("thread.max_queue_size");
897 }
898
899 // Logger config
900 if (old_cfg.logger.level != new_cfg.logger.level) {
901 changes.push_back("logger.level");
902 }
903 if (old_cfg.logger.async != new_cfg.logger.async) {
904 changes.push_back("logger.async");
905 }
906 if (old_cfg.logger.buffer_size != new_cfg.logger.buffer_size) {
907 changes.push_back("logger.buffer_size");
908 }
909 if (old_cfg.logger.file_path != new_cfg.logger.file_path) {
910 changes.push_back("logger.file_path");
911 }
912 if (old_cfg.logger.writers != new_cfg.logger.writers) {
913 changes.push_back("logger.writers");
914 }
915
916 // Monitoring config
917 if (old_cfg.monitoring.enabled != new_cfg.monitoring.enabled) {
918 changes.push_back("monitoring.enabled");
919 }
921 changes.push_back("monitoring.metrics_interval");
922 }
923 if (old_cfg.monitoring.tracing.enabled != new_cfg.monitoring.tracing.enabled) {
924 changes.push_back("monitoring.tracing.enabled");
925 }
927 changes.push_back("monitoring.tracing.sampling_rate");
928 }
929
930 // Database config
931 if (old_cfg.database.backend != new_cfg.database.backend) {
932 changes.push_back("database.backend");
933 }
934 if (old_cfg.database.connection_string != new_cfg.database.connection_string) {
935 changes.push_back("database.connection_string");
936 }
937
938 // Network config
939 if (old_cfg.network.tls.enabled != new_cfg.network.tls.enabled) {
940 changes.push_back("network.tls.enabled");
941 }
942 if (old_cfg.network.compression != new_cfg.network.compression) {
943 changes.push_back("network.compression");
944 }
945 if (old_cfg.network.buffer_size != new_cfg.network.buffer_size) {
946 changes.push_back("network.buffer_size");
947 }
948
949 return changes;
950 }
951
955 void add_to_history(const unified_config& config) {
956 std::lock_guard<std::mutex> lock(history_mutex_);
957
958 config_snapshot snapshot;
959 snapshot.version = version_.load();
960 snapshot.timestamp = std::chrono::system_clock::now();
961 snapshot.config = config;
962
963 history_.push_back(snapshot);
964
965 // Trim history if needed
966 while (history_.size() > max_history_) {
967 history_.pop_front();
968 }
969 }
970
975 std::lock_guard<std::mutex> lock(events_mutex_);
976 events_.push_back(event);
977
978 // Keep only recent events
979 while (events_.size() > 100) {
980 events_.pop_front();
981 }
982 }
983
987 void notify_change(const unified_config& old_cfg, const unified_config& new_cfg) {
988 std::lock_guard<std::mutex> lock(callbacks_mutex_);
989 for (const auto& callback : change_callbacks_) {
990 try {
991 callback(old_cfg, new_cfg);
992 } catch (...) {
993 // Ignore callback exceptions
994 }
995 }
996 }
997
1001 void notify_error(const std::string& message) {
1002 std::lock_guard<std::mutex> lock(callbacks_mutex_);
1003 for (const auto& callback : error_callbacks_) {
1004 try {
1005 callback(message);
1006 } catch (...) {
1007 // Ignore callback exceptions
1008 }
1009 }
1010 }
1011
1012 // Configuration state
1013 std::string config_path_;
1015 mutable std::shared_mutex config_mutex_;
1016
1017 // Version tracking
1018 std::atomic<uint64_t> version_;
1019
1020 // History
1022 mutable std::mutex history_mutex_;
1023 std::deque<config_snapshot> history_;
1024
1025 // Events
1026 mutable std::mutex events_mutex_;
1027 std::deque<config_change_event> events_;
1028
1029 // Callbacks
1031 std::vector<change_callback> change_callbacks_;
1032 std::vector<error_callback> error_callbacks_;
1033
1034 // Watch thread
1035 std::atomic<bool> running_;
1036 std::thread watch_thread_;
1037
1038 // Platform-specific handles
1039#if defined(__linux__)
1040 std::atomic<int> inotify_fd_;
1041 std::atomic<int> watch_fd_;
1042 int shutdown_fd_;
1043#elif defined(__APPLE__) || defined(__FreeBSD__)
1044 std::atomic<int> kqueue_fd_;
1045 int file_fd_;
1046 std::mutex file_fd_mutex_;
1047#elif defined(_WIN32)
1048 HANDLE dir_handle_;
1049#endif
1050};
1051
1052} // namespace kcenon::common::config
Result type for error handling with member function support.
Definition core.cppm:165
static Result< T > ok(U &&value)
Create a successful result with value (static factory)
Definition core.h:223
static unified_config defaults()
Get default configuration.
static VoidResult validate(const unified_config &config)
Validate a configuration.
static Result< unified_config > load(const std::string &path)
Load configuration from a YAML file.
Monitors configuration files for changes and supports hot-reload.
std::vector< config_snapshot > history(size_t count=0) const
Get configuration history snapshots.
VoidResult rollback(uint64_t target_version)
Rollback to a previous configuration version.
VoidResult init_platform_watcher()
Initialize platform-specific file watching.
std::vector< config_change_event > recent_events(size_t count=10) const
Get recent change events.
std::deque< config_change_event > events_
config_watcher & operator=(config_watcher &&)=delete
bool is_running() const
Check if the watcher is currently running.
VoidResult reload()
Manually trigger a configuration reload.
static std::vector< std::string > get_changed_fields(const unified_config &old_cfg, const unified_config &new_cfg)
Compare two configurations and return changed field paths.
void signal_watcher_shutdown()
Signal the watch thread to wake up and exit.
VoidResult start()
Start watching the configuration file for changes.
config_watcher(config_watcher &&)=delete
void on_change(change_callback callback)
Register a callback for configuration changes.
config_watcher(const std::string &config_path, size_t max_history=10)
Construct a config_watcher for the specified file.
void stop()
Stop watching the configuration file.
std::vector< error_callback > error_callbacks_
void notify_change(const unified_config &old_cfg, const unified_config &new_cfg)
Notify all registered change callbacks.
const unified_config & current() const
Get the current configuration.
void on_error(error_callback callback)
Register a callback for reload errors.
std::vector< change_callback > change_callbacks_
uint64_t version() const
Get the current configuration version.
const std::string & config_path() const
Get the path to the configuration file being watched.
void add_event(const config_change_event &event)
Add a change event to the event log.
void watch_loop()
Main watch loop - dispatches to platform-specific implementation.
void add_to_history(const unified_config &config)
Add a configuration to history.
std::function< void(const std::string &error_message)> error_callback
Callback type for reload errors.
config_watcher(const config_watcher &)=delete
void notify_error(const std::string &message)
Notify all registered error callbacks.
VoidResult do_reload()
Perform the actual configuration reload.
std::deque< config_snapshot > history_
config_watcher & operator=(const config_watcher &)=delete
void cleanup_platform_watcher()
Cleanup platform-specific resources (call after thread join).
~config_watcher()
Destructor. Automatically stops watching if running.
std::function< void(const unified_config &old_config, const unified_config &new_config)> change_callback
Callback type for configuration changes.
YAML-based configuration loader for the unified system.
bool is_hot_reloadable(const std::string &field_path)
Check if a configuration field supports hot-reload.
Result< T > make_error(int code, const std::string &message, const std::string &module="")
Create an error result with code and message.
Definition utilities.h:91
Result< std::monostate > VoidResult
Specialized Result for void operations.
Definition core.h:70
Umbrella header for Result<T> type and related utilities.
Information about a configuration change event.
std::chrono::system_clock::time_point timestamp
Timestamp of the change.
std::string error_message
Error message if change failed.
bool success
Whether the change was successful.
std::vector< std::string > changed_fields
List of changed field paths.
uint64_t version
Configuration version (incrementing counter)
Represents a configuration snapshot for version history.
std::chrono::system_clock::time_point timestamp
Timestamp when this configuration was active.
uint64_t version
Configuration version number.
unified_config config
The configuration data.
std::string connection_string
Connection string or URI.
std::string backend
Database backend: "postgresql", "mysql", "sqlite", "mongodb", "redis".
bool async
Enable async logging.
std::vector< std::string > writers
List of writers: "console", "file", "rotating_file", "network", "json".
std::string file_path
Log file path (for file writers)
size_t buffer_size
Async buffer size in bytes.
std::string level
Log level: "trace", "debug", "info", "warn", "error", "critical", "off".
std::chrono::milliseconds metrics_interval
Metrics collection interval.
tracing_config tracing
Tracing configuration.
size_t buffer_size
Send/receive buffer size.
tls_config tls
TLS configuration.
std::string compression
Compression type: "none", "lz4", "gzip", "deflate", "zstd".
std::string queue_type
Queue type: "mutex", "lockfree", "bounded".
size_t pool_size
Number of worker threads (default: hardware concurrency)
size_t max_queue_size
Maximum queue size (for bounded queue)
double sampling_rate
Sampling rate (0.0 to 1.0)
Root configuration structure for the unified system.
network_config network
Network system configuration.
logger_config logger
Logger system configuration.
thread_config thread
Thread system configuration.
monitoring_config monitoring
Monitoring system configuration.
database_config database
Database system configuration.
Generic event structure for the event bus.
Definition event_bus.h:111
Unified configuration schema for the entire system.