From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id A6E131FF179 for ; Wed, 7 Jan 2026 10:15:23 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 4046E7115; Wed, 7 Jan 2026 10:16:07 +0100 (CET) From: Kefu Chai To: pve-devel@lists.proxmox.com Date: Tue, 6 Jan 2026 22:24:32 +0800 Message-ID: <20260106142440.2368585-9-k.chai@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260106142440.2368585-1-k.chai@proxmox.com> References: <20260106142440.2368585-1-k.chai@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1767709488594 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.048 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LOTSOFHASH 0.25 Emails with lots of hash-like gibberish SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_SBL_A 0.1 Contains URL's A record listed in the Spamhaus SBL blocklist [188.114.97.3, 188.114.96.3] X-Mailman-Approved-At: Wed, 07 Jan 2026 10:16:04 +0100 Subject: [pve-devel] [PATCH pve-cluster 08/15] pmxcfs-rs: add pmxcfs-services crate X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox VE development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" Add service lifecycle management framework providing: - Service trait: Lifecycle interface for async services - ServiceManager: Orchestrates multiple services - Automatic retry logic for failed services - Event-driven dispatching via file descriptors - Graceful shutdown coordination This is a generic framework with no pmxcfs-specific dependencies, only requiring tokio, async-trait, and standard error handling. It replaces the C version's qb_loop-based event management. Signed-off-by: Kefu Chai --- src/pmxcfs-rs/Cargo.lock | 1798 +---------------- src/pmxcfs-rs/Cargo.toml | 1 + src/pmxcfs-rs/pmxcfs-services/Cargo.toml | 17 + src/pmxcfs-rs/pmxcfs-services/README.md | 167 ++ src/pmxcfs-rs/pmxcfs-services/src/error.rs | 37 + src/pmxcfs-rs/pmxcfs-services/src/lib.rs | 16 + src/pmxcfs-rs/pmxcfs-services/src/manager.rs | 477 +++++ src/pmxcfs-rs/pmxcfs-services/src/service.rs | 173 ++ .../pmxcfs-services/tests/service_tests.rs | 808 ++++++++ 9 files changed, 1778 insertions(+), 1716 deletions(-) create mode 100644 src/pmxcfs-rs/pmxcfs-services/Cargo.toml create mode 100644 src/pmxcfs-rs/pmxcfs-services/README.md create mode 100644 src/pmxcfs-rs/pmxcfs-services/src/error.rs create mode 100644 src/pmxcfs-rs/pmxcfs-services/src/lib.rs create mode 100644 src/pmxcfs-rs/pmxcfs-services/src/manager.rs create mode 100644 src/pmxcfs-rs/pmxcfs-services/src/service.rs create mode 100644 src/pmxcfs-rs/pmxcfs-services/tests/service_tests.rs diff --git a/src/pmxcfs-rs/Cargo.lock b/src/pmxcfs-rs/Cargo.lock index 31a30e13..f0ec6231 100644 --- a/src/pmxcfs-rs/Cargo.lock +++ b/src/pmxcfs-rs/Cargo.lock @@ -2,98 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - [[package]] name = "anyhow" version = "1.0.100" @@ -108,248 +16,27 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bindgen" -version = "0.71.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" -dependencies = [ - "bitflags 2.10.0", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.111", + "syn", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" -[[package]] -name = "cc" -version = "1.2.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom 7.1.3", -] - [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clap" -version = "4.5.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "clap_lex" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - [[package]] name = "errno" version = "0.3.14" @@ -361,1051 +48,139 @@ dependencies = [ ] [[package]] -name = "fallible-iterator" -version = "0.3.0" +name = "futures-core" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" +name = "futures-sink" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] -name = "fastrand" -version = "2.3.0" +name = "libc" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] -name = "filetime" -version = "0.2.26" +name = "lock_api" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "cfg-if", - "libc", - "libredox", - "windows-sys 0.60.2", + "scopeguard", ] [[package]] -name = "find-msvc-tools" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" - -[[package]] -name = "flate2" -version = "1.1.5" +name = "mio" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ - "crc32fast", - "miniz_oxide", + "libc", + "wasi", + "windows-sys 0.61.2", ] [[package]] -name = "futures" -version = "0.3.31" +name = "once_cell" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "futures-channel" -version = "0.3.31" +name = "parking_lot" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ - "futures-core", - "futures-sink", + "lock_api", + "parking_lot_core", ] [[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" +name = "parking_lot_core" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ - "futures-core", - "futures-task", - "futures-util", + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", ] [[package]] -name = "futures-io" -version = "0.3.31" +name = "pin-project-lite" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +name = "pmxcfs-api-types" +version = "9.0.6" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", + "libc", + "thiserror 1.0.69", ] [[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +name = "pmxcfs-config" +version = "9.0.6" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", + "parking_lot", ] [[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "indexmap" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "js-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.178" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libredox" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" -dependencies = [ - "bitflags 2.10.0", - "libc", - "redox_syscall 0.7.0", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "memmap2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "nix" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "libc", - "memoffset", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_enum" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" -dependencies = [ - "num_enum_derive 0.5.11", -] - -[[package]] -name = "num_enum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" -dependencies = [ - "num_enum_derive 0.7.5", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "pmxcfs" -version = "9.0.6" -dependencies = [ - "anyhow", - "async-trait", - "bincode", - "bytemuck", - "bytes", - "chrono", - "clap", - "filetime", - "futures", - "libc", - "nix", - "num_enum 0.7.5", - "parking_lot", - "pmxcfs-api-types", - "pmxcfs-config", - "pmxcfs-dfsm", - "pmxcfs-ipc", - "pmxcfs-memdb", - "pmxcfs-rrd", - "pmxcfs-services", - "pmxcfs-status", - "proxmox-fuse", - "rust-corosync", - "serde", - "serde_json", - "sha2", - "tempfile", - "thiserror 1.0.69", - "tokio", - "tokio-util", - "tracing", - "tracing-subscriber", - "users", -] - -[[package]] -name = "pmxcfs-api-types" -version = "9.0.6" -dependencies = [ - "libc", - "thiserror 1.0.69", -] - -[[package]] -name = "pmxcfs-config" -version = "9.0.6" -dependencies = [ - "parking_lot", -] - -[[package]] -name = "pmxcfs-dfsm" -version = "9.0.6" -dependencies = [ - "anyhow", - "async-trait", - "bincode", - "bytemuck", - "libc", - "num_enum 0.7.5", - "parking_lot", - "pmxcfs-api-types", - "pmxcfs-memdb", - "pmxcfs-services", - "rust-corosync", - "serde", - "tempfile", - "thiserror 1.0.69", - "tokio", - "tracing", -] - -[[package]] -name = "pmxcfs-ipc" -version = "9.0.6" -dependencies = [ - "anyhow", - "async-trait", - "libc", - "memmap2", - "nix", - "parking_lot", - "pmxcfs-test-utils", - "tempfile", - "tokio", - "tokio-util", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "pmxcfs-logger" -version = "0.1.0" -dependencies = [ - "anyhow", - "parking_lot", - "serde", - "serde_json", - "tempfile", - "tracing", -] - -[[package]] -name = "pmxcfs-memdb" -version = "9.0.6" -dependencies = [ - "anyhow", - "bincode", - "bytes", - "libc", - "parking_lot", - "pmxcfs-api-types", - "rusqlite", - "serde", - "sha2", - "tempfile", - "tracing", -] - -[[package]] -name = "pmxcfs-rrd" -version = "9.0.6" +name = "pmxcfs-services" +version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "chrono", - "rrd", - "rrdcached-client", - "tempfile", - "tokio", - "tracing", -] - -[[package]] -name = "pmxcfs-services" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "parking_lot", - "pmxcfs-test-utils", - "scopeguard", - "thiserror 2.0.17", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "pmxcfs-status" -version = "9.0.6" -dependencies = [ - "anyhow", - "chrono", - "parking_lot", - "pmxcfs-api-types", - "pmxcfs-logger", - "pmxcfs-memdb", - "pmxcfs-rrd", - "procfs", - "tempfile", - "tokio", - "tracing", -] - -[[package]] -name = "pmxcfs-test-utils" -version = "9.0.6" -dependencies = [ - "anyhow", - "libc", - "parking_lot", - "pmxcfs-api-types", - "pmxcfs-config", - "pmxcfs-memdb", - "pmxcfs-status", - "tempfile", - "tokio", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.111", -] - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", -] - -[[package]] -name = "proc-macro2" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "procfs" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" -dependencies = [ - "bitflags 2.10.0", - "chrono", - "flate2", - "hex", - "procfs-core", - "rustix 0.38.44", -] - -[[package]] -name = "procfs-core" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" -dependencies = [ - "bitflags 2.10.0", - "chrono", - "hex", -] - -[[package]] -name = "proxmox-fuse" -version = "1.0.0" -dependencies = [ - "anyhow", - "cc", - "futures", - "libc", - "tokio", - "tokio-stream", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_syscall" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "rrd" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9076fed5ab29d1b4a6e8256c3ac78ec5506843f9eb3daaab9e9077b4d603bb3" -dependencies = [ - "bitflags 2.10.0", - "chrono", - "itertools 0.14.0", - "log", - "nom 7.1.3", - "regex", - "rrd-sys", - "thiserror 2.0.17", -] - -[[package]] -name = "rrd-sys" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f01965ba4fa5116984978aa941a92bdcc60001f757abbaa1234d7e40eeaba3d" -dependencies = [ - "bindgen", - "pkg-config", -] - -[[package]] -name = "rrdcached-client" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57dfd6f5a3094934b1f0813199b7571be5bde0bcc985005fe5a3c3d6a738d4cd" -dependencies = [ - "nom 8.0.0", - "thiserror 2.0.17", - "tokio", -] - -[[package]] -name = "rusqlite" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d" -dependencies = [ - "bitflags 2.10.0", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", -] - -[[package]] -name = "rust-corosync" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c82a532b982d3a42e804beff9088d05ff3f5f5ee8cc552696dc3550ba13039" -dependencies = [ - "bitflags 1.3.2", - "lazy_static", - "num_enum 0.5.11", - "pkg-config", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", + "parking_lot", + "scopeguard", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tracing", ] [[package]] -name = "serde_json" -version = "1.0.148" +name = "proc-macro2" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", + "unicode-ident", ] [[package]] -name = "sha2" -version = "0.10.9" +name = "quote" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "proc-macro2", ] [[package]] -name = "sharded-slab" -version = "0.1.7" +name = "redox_syscall" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "lazy_static", + "bitflags", ] [[package]] -name = "shlex" -version = "1.3.0" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "signal-hook-registry" @@ -1417,18 +192,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - [[package]] name = "smallvec" version = "1.15.1" @@ -1445,23 +208,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.111" @@ -1473,19 +219,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tempfile" -version = "3.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" -dependencies = [ - "fastrand", - "getrandom", - "once_cell", - "rustix 1.1.3", - "windows-sys 0.61.2", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -1512,7 +245,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -1523,16 +256,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", + "syn", ] [[package]] @@ -1560,18 +284,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", -] - -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", + "syn", ] [[package]] @@ -1587,53 +300,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap", - "toml_datetime 0.6.11", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.23.10+spec-1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" -dependencies = [ - "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "winnow 0.7.14", -] - -[[package]] -name = "toml_parser" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" -dependencies = [ - "winnow 0.7.14", -] - [[package]] name = "tracing" version = "0.1.44" @@ -1653,7 +319,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -1663,219 +329,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", ] -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" -[[package]] -name = "users" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" -dependencies = [ - "libc", - "log", -] - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.111", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -1887,22 +367,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - [[package]] name = "windows-targets" version = "0.53.5" @@ -1910,158 +374,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "zerocopy" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "zmij" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3280a1b827474fcd5dbef4b35a674deb52ba5c312363aef9135317df179d81b" diff --git a/src/pmxcfs-rs/Cargo.toml b/src/pmxcfs-rs/Cargo.toml index 8fe06b88..b00ca68f 100644 --- a/src/pmxcfs-rs/Cargo.toml +++ b/src/pmxcfs-rs/Cargo.toml @@ -8,6 +8,7 @@ members = [ "pmxcfs-memdb", # In-memory database with SQLite persistence "pmxcfs-status", # Status monitoring and RRD data management "pmxcfs-test-utils", # Test utilities and helpers (dev-only) + "pmxcfs-services", # Service framework for automatic retry and lifecycle management ] resolver = "2" diff --git a/src/pmxcfs-rs/pmxcfs-services/Cargo.toml b/src/pmxcfs-rs/pmxcfs-services/Cargo.toml new file mode 100644 index 00000000..7991b913 --- /dev/null +++ b/src/pmxcfs-rs/pmxcfs-services/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "pmxcfs-services" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0" +async-trait = "0.1" +tokio = { version = "1.41", features = ["full"] } +tokio-util = "0.7" +tracing = "0.1" +thiserror = "2.0" +parking_lot = "0.12" +scopeguard = "1.2" + +[dev-dependencies] +pmxcfs-test-utils = { path = "../pmxcfs-test-utils" } diff --git a/src/pmxcfs-rs/pmxcfs-services/README.md b/src/pmxcfs-rs/pmxcfs-services/README.md new file mode 100644 index 00000000..ca17e3e9 --- /dev/null +++ b/src/pmxcfs-rs/pmxcfs-services/README.md @@ -0,0 +1,167 @@ +# pmxcfs-services + +**Service Management Framework** for pmxcfs - tokio-based replacement for qb_loop. + +This crate provides a robust, async service management framework with automatic retry, event-driven dispatching, periodic timers, and graceful shutdown. It replaces the C implementation's libqb loop with a modern tokio-based architecture. + +## Overview + +The service framework manages long-running services that need: +- **Automatic initialization retry** when connections fail +- **Event-driven dispatching** for file descriptor-based services (Corosync) +- **Periodic timers** for maintenance tasks +- **Error tracking** with throttled logging +- **Graceful shutdown** with resource cleanup + +## Key Concepts + +- **Service**: A trait implementing lifecycle methods (`initialize`, `dispatch`, `finalize`) +- **ServiceManager**: Orchestrates multiple services, handles retries, timers, and shutdown +- **ManagedService**: Internal wrapper that tracks state and handles recovery + +## Service Trait + +The `Service` trait defines the lifecycle of a managed service: + +```rust +#[async_trait] +pub trait Service: Send + Sync { + fn name(&self) -> &str; + async fn initialize(&mut self) -> Result; + async fn dispatch(&mut self) -> Result; + async fn finalize(&mut self) -> Result<()>; + + // Optional overrides: + fn timer_period(&self) -> Option { None } + async fn timer_callback(&mut self) -> Result<()> { Ok(()) } + fn is_restartable(&self) -> bool { true } + fn retry_interval(&self) -> Duration { Duration::from_secs(5) } + fn dispatch_interval(&self) -> Duration { Duration::from_millis(100) } +} +``` + +## InitResult + +Services return `InitResult` to indicate their dispatch mode: + +**WithFileDescriptor(fd)**: +- **Use case**: Corosync services (CPG, quorum, cmap) +- **Behavior**: `dispatch()` called when fd becomes readable +- **Efficiency**: Event-driven, no polling overhead +- **Example**: ClusterDatabaseService, QuorumService + +**NoFileDescriptor**: +- **Use case**: Services without external event sources +- **Behavior**: `dispatch()` called periodically at `dispatch_interval()` +- **Efficiency**: Polling overhead (default: 100ms interval) + +## ServiceManager + +Orchestrates multiple services with automatic management: + +```rust +let mut manager = ServiceManager::new(); +manager.add_service(Box::new(MyService::new())); +manager.add_service(Box::new(AnotherService::new())); +let handle = manager.spawn(); // Returns JoinHandle for lifecycle control +// ... later ... +handle.abort(); // Gracefully shuts down all services +``` + +### Features + +1. **Automatic Retry**: Failed services automatically retry initialization +2. **Event-Driven**: Services with file descriptors use tokio AsyncFd (no polling) +3. **Timers**: Optional periodic callbacks for maintenance +4. **Error Tracking**: Counts consecutive failures, throttles error logs +5. **Graceful Shutdown**: Finalizes all services on exit + +## Usage Example + +```rust +use pmxcfs_services::{Service, InitResult, DispatchAction, ServiceManager}; + +struct MyService { + fd: Option, +} + +#[async_trait] +impl Service for MyService { + fn name(&self) -> &str { "my-service" } + + async fn initialize(&mut self) -> Result { + let fd = connect_to_external_service()?; + self.fd = Some(fd); + Ok(InitResult::WithFileDescriptor(fd)) + } + + async fn dispatch(&mut self) -> Result { + handle_events()?; + Ok(DispatchAction::Continue) + } + + async fn finalize(&mut self) -> Result<()> { + close_connection(self.fd.take())?; + Ok(()) + } +} +``` + +## C to Rust Mapping + +### Data Structures + +| C Type | Rust Type | Notes | +|--------|-----------|-------| +| `cfs_loop_t` | `ServiceManager` | Event loop manager | +| `cfs_service_t` | `dyn Service` | Service trait | +| `cfs_service_callbacks_t` | (trait methods) | Callbacks as trait methods | + +### Functions + +| C Function | Rust Equivalent | Location | +|-----------|-----------------|----------| +| `cfs_loop_new()` | `ServiceManager::new()` | manager.rs | +| `cfs_loop_add_service()` | `ServiceManager::add_service()` | manager.rs | +| `cfs_loop_start_worker()` | `ServiceManager::spawn()` | manager.rs | +| `cfs_loop_stop_worker()` | `handle.abort()` | Tokio abort | +| `cfs_service_new()` | (struct + impl Service) | User code | + +## Key Differences from C Implementation + +### Event Loop Architecture + +**C Version (loop.c)**: +- Uses libqb's `qb_loop` event loop +- Manual fd registration with `qb_loop_poll_add()` +- Single-threaded callback-based model +- Priority levels for services + +**Rust Version**: +- Uses tokio async runtime +- Automatic fd monitoring with `AsyncFd` +- Concurrent task-based model +- No priority levels (all equal) + +### Concurrency + +**C Version**: +- Single-threaded qb_loop +- Callbacks run sequentially + +**Rust Version**: +- Multi-threaded tokio runtime +- Services can run in parallel + +## References + +### C Implementation +- `src/pmxcfs/loop.c` / `loop.h` - Service loop + +### Related Crates +- **pmxcfs-dfsm**: Uses Service trait for ClusterDatabaseService, StatusSyncService +- **pmxcfs**: Uses ServiceManager to orchestrate all cluster services + +### External Dependencies +- **tokio**: Async runtime and I/O +- **async-trait**: Async methods in traits diff --git a/src/pmxcfs-rs/pmxcfs-services/src/error.rs b/src/pmxcfs-rs/pmxcfs-services/src/error.rs new file mode 100644 index 00000000..c0dde47b --- /dev/null +++ b/src/pmxcfs-rs/pmxcfs-services/src/error.rs @@ -0,0 +1,37 @@ +//! Error types for the service framework + +use thiserror::Error; + +/// Errors that can occur during service operations +#[derive(Error, Debug)] +pub enum ServiceError { + /// Service initialization failed + #[error("Failed to initialize service: {0}")] + InitializationFailed(String), + + /// Service dispatch failed + #[error("Failed to dispatch service events: {0}")] + DispatchFailed(String), + + /// Service finalization failed + #[error("Failed to finalize service: {0}")] + FinalizationFailed(String), + + /// Timer callback failed + #[error("Timer callback failed: {0}")] + TimerFailed(String), + + /// Service is not running + #[error("Service is not running")] + NotRunning, + + /// Service is already running + #[error("Service is already running")] + AlreadyRunning, + + /// Generic error with context + #[error("{0}")] + Other(#[from] anyhow::Error), +} + +pub type Result = std::result::Result; diff --git a/src/pmxcfs-rs/pmxcfs-services/src/lib.rs b/src/pmxcfs-rs/pmxcfs-services/src/lib.rs new file mode 100644 index 00000000..cf894cc5 --- /dev/null +++ b/src/pmxcfs-rs/pmxcfs-services/src/lib.rs @@ -0,0 +1,16 @@ +//! Service framework for pmxcfs +//! +//! This crate provides a robust, tokio-based service management framework with: +//! - Automatic retry on failure +//! - Event-driven file descriptor monitoring +//! - Periodic timer callbacks +//! - Error tracking and throttled logging +//! - Graceful shutdown + +mod error; +mod manager; +mod service; + +pub use error::{Result, ServiceError}; +pub use manager::ServiceManager; +pub use service::{DispatchAction, InitResult, Service}; diff --git a/src/pmxcfs-rs/pmxcfs-services/src/manager.rs b/src/pmxcfs-rs/pmxcfs-services/src/manager.rs new file mode 100644 index 00000000..48c09c15 --- /dev/null +++ b/src/pmxcfs-rs/pmxcfs-services/src/manager.rs @@ -0,0 +1,477 @@ +//! Service manager for orchestrating multiple managed services +//! +//! The ServiceManager handles automatic retry, error tracking, event dispatching, +//! and timer callbacks for all registered services. It uses tokio for async I/O +//! and provides graceful shutdown capabilities. + +use crate::service::{DispatchAction, InitResult, Service}; +use parking_lot::RwLock; +use std::collections::HashMap; +use std::os::unix::io::{AsRawFd, RawFd}; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::io::unix::AsyncFd; +use tokio::task::JoinHandle; +use tokio::time::{MissedTickBehavior, interval}; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info, warn}; + +/// Shared state for a managed service +struct ManagedService { + /// The service implementation (wrapped in Mutex for interior mutability) + service: tokio::sync::Mutex>, + /// Current service state + state: RwLock, + /// Consecutive error count (reset on successful initialization) + error_count: RwLock, + /// Last initialization attempt timestamp + last_init_attempt: RwLock>, + /// Async file descriptor for event monitoring (if applicable) + async_fd: RwLock>>>, + /// Last timer callback invocation + last_timer_invoke: RwLock>, +} + +/// Service state +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ServiceState { + /// Service not yet initialized + Uninitialized, + /// Service currently initializing + Initializing, + /// Service running successfully + Running, + /// Service failed, awaiting retry + Failed, +} + +/// Wrapper for raw file descriptor to implement AsRawFd +struct FdWrapper(RawFd); + +impl AsRawFd for FdWrapper { + fn as_raw_fd(&self) -> RawFd { + self.0 + } +} + +impl Drop for FdWrapper { + fn drop(&mut self) { + // File descriptor ownership is managed by the service + // We just monitor it, so don't close it here + } +} + +/// Service manager for orchestrating multiple services +/// +/// The ServiceManager provides: +/// - Automatic retry of failed initializations +/// - Event-driven dispatching for file descriptor-based services +/// - Periodic polling for services without file descriptors +/// - Timer callbacks for periodic maintenance +/// - Error tracking and throttled logging +/// - Graceful shutdown +pub struct ServiceManager { + /// Registered services by name + services: HashMap>, + /// Cancellation token for graceful shutdown + shutdown_token: CancellationToken, +} + +impl ServiceManager { + /// Create a new service manager + pub fn new() -> Self { + Self { + services: HashMap::new(), + shutdown_token: CancellationToken::new(), + } + } + + /// Add a service to be managed + /// + /// Services will be started when `run()` is called. + /// + /// # Panics + /// + /// Panics if a service with the same name is already registered. + pub fn add_service(&mut self, service: Box) { + let name = service.name().to_string(); + + if self.services.contains_key(&name) { + panic!("Service '{name}' is already registered"); + } + + let managed = Arc::new(ManagedService { + service: tokio::sync::Mutex::new(service), + state: RwLock::new(ServiceState::Uninitialized), + error_count: RwLock::new(0), + last_init_attempt: RwLock::new(None), + async_fd: RwLock::new(None), + last_timer_invoke: RwLock::new(None), + }); + + self.services.insert(name, managed); + } + + /// Get a handle to trigger shutdown + /// + /// Call `cancel()` on the returned token to initiate graceful shutdown. + pub fn shutdown_token(&self) -> CancellationToken { + self.shutdown_token.clone() + } + + /// Spawn the service manager in a background task + /// + /// Returns a JoinHandle that can be used to await completion. + /// To gracefully shut down, call `.shutdown_token().cancel()` then await the handle. + /// + /// # Example + /// + /// ```ignore + /// let shutdown_token = manager.shutdown_token(); + /// let handle = manager.spawn(); + /// // ... later ... + /// shutdown_token.cancel(); // Trigger graceful shutdown + /// handle.await; // Wait for shutdown to complete + /// ``` + pub fn spawn(self) -> JoinHandle<()> { + tokio::spawn(async move { self.run().await }) + } + + /// Run the service manager (private - use spawn() instead) + /// + /// This starts all registered services and runs until shutdown is requested. + /// Services are automatically retried on failure according to their configuration. + async fn run(self) { + info!( + "Starting ServiceManager with {} services", + self.services.len() + ); + + let services = Arc::new(self.services); + + // Spawn retry task for failed services + let retry_handle = Self::spawn_retry_task_static(Arc::clone(&services)); + + // Spawn timer callback task + let timer_handle = Self::spawn_timer_task_static(Arc::clone(&services)); + + // Spawn dispatch tasks for each service + let dispatch_handles = Self::spawn_dispatch_tasks_static(Arc::clone(&services)); + + // Wait for shutdown signal + self.shutdown_token.cancelled().await; + + // Graceful shutdown sequence + info!("ServiceManager shutting down..."); + + // Shutdown all services gracefully + Self::shutdown_all_services_static(&services).await; + + // Cancel background tasks + retry_handle.abort(); + timer_handle.abort(); + for handle in dispatch_handles { + handle.abort(); + } + + info!("ServiceManager stopped"); + } + + /// Spawn task that retries failed service initializations + fn spawn_retry_task_static( + services: Arc>>, + ) -> JoinHandle<()> { + tokio::spawn(async move { + let mut retry_interval = interval(Duration::from_secs(1)); + retry_interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + + loop { + retry_interval.tick().await; + Self::retry_failed_services(&services).await; + } + }) + } + + /// Retry initialization for failed services + async fn retry_failed_services(services: &HashMap>) { + for (name, managed) in services { + // Check if service needs retry + let state = *managed.state.read(); + if state != ServiceState::Uninitialized { + continue; + } + + let (is_restartable, retry_interval) = { + let service = managed.service.lock().await; + (service.is_restartable(), service.retry_interval()) + }; + + // Check if this is a retry or first attempt + let now = Instant::now(); + let is_first_attempt = managed.last_init_attempt.read().is_none(); + + // Allow first attempt for all services, but block retries for non-restartable services + if !is_first_attempt && !is_restartable { + continue; + } + + // Check retry throttle (only for retries) + if let Some(last) = *managed.last_init_attempt.read() + && now.duration_since(last) < retry_interval + { + continue; + } + + // Attempt initialization + *managed.last_init_attempt.write() = Some(now); + *managed.state.write() = ServiceState::Initializing; + + debug!(service = %name, "Attempting to initialize service"); + + let mut service = managed.service.lock().await; + + match service.initialize().await { + Ok(InitResult::WithFileDescriptor(fd)) => match AsyncFd::new(FdWrapper(fd)) { + Ok(async_fd) => { + *managed.async_fd.write() = Some(Arc::new(async_fd)); + *managed.state.write() = ServiceState::Running; + *managed.error_count.write() = 0; + info!(service = %name, fd, "Service initialized successfully"); + } + Err(e) => { + error!(service = %name, fd, error = %e, "Failed to register fd"); + *managed.state.write() = ServiceState::Failed; + *managed.error_count.write() += 1; + } + }, + Ok(InitResult::NoFileDescriptor) => { + *managed.state.write() = ServiceState::Running; + *managed.error_count.write() = 0; + info!(service = %name, "Service initialized successfully (no fd)"); + } + Err(e) => { + let err_count = { + let mut count = managed.error_count.write(); + *count += 1; + *count + }; + + // Only log first failure to avoid spam + if err_count == 1 { + error!(service = %name, error = %e, "Failed to initialize service"); + } else { + debug!(service = %name, attempt = err_count, error = %e, "Service initialization failed"); + } + + *managed.state.write() = ServiceState::Uninitialized; + } + } + } + } + + /// Spawn task that invokes timer callbacks + fn spawn_timer_task_static( + services: Arc>>, + ) -> JoinHandle<()> { + tokio::spawn(async move { + let mut timer_interval = interval(Duration::from_secs(1)); + timer_interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + + loop { + timer_interval.tick().await; + Self::invoke_timer_callbacks(&services).await; + } + }) + } + + /// Invoke timer callbacks for running services + async fn invoke_timer_callbacks(services: &HashMap>) { + let now = Instant::now(); + + for (name, managed) in services { + // Check if service is running + if *managed.state.read() != ServiceState::Running { + continue; + } + + let Some(period) = ({ managed.service.lock().await.timer_period() }) else { + continue; + }; + + // Check if it's time to invoke timer + let should_invoke = match *managed.last_timer_invoke.read() { + Some(last) => now.duration_since(last) >= period, + None => true, // First invocation + }; + + if !should_invoke { + continue; + } + + *managed.last_timer_invoke.write() = Some(now); + + debug!(service = %name, "Invoking timer callback"); + + let mut service = managed.service.lock().await; + + if let Err(e) = service.timer_callback().await { + warn!(service = %name, error = %e, "Timer callback failed"); + } + } + } + + /// Spawn dispatch tasks for all services + fn spawn_dispatch_tasks_static( + services: Arc>>, + ) -> Vec> { + let mut handles = Vec::new(); + + for (name, managed) in services.iter() { + let name = name.clone(); + let managed = Arc::clone(managed); + + let handle = tokio::spawn(async move { + loop { + // Wait for service to be running + loop { + tokio::time::sleep(Duration::from_millis(100)).await; + let state = *managed.state.read(); + if state == ServiceState::Running { + break; + } + } + + // Dispatch based on service type + let async_fd = managed.async_fd.read().clone(); + + if let Some(fd) = async_fd { + // Event-driven dispatch + Self::dispatch_with_fd(&name, &managed, &fd).await; + } else { + // Polling dispatch + Self::dispatch_polling(&name, &managed).await; + } + } + }); + + handles.push(handle); + } + + handles + } + + /// Dispatch events for service with file descriptor + async fn dispatch_with_fd( + name: &str, + managed: &Arc, + async_fd: &Arc>, + ) { + loop { + let readable = match async_fd.readable().await { + Ok(r) => r, + Err(e) => { + warn!(service = %name, error = %e, "Error waiting for fd readability"); + break; + } + }; + + let mut guard = readable; + let mut service = managed.service.lock().await; + + match service.dispatch().await { + Ok(DispatchAction::Continue) => { + guard.clear_ready(); + } + Ok(DispatchAction::Reinitialize) => { + info!(service = %name, "Service requested reinitialization"); + guard.clear_ready(); + drop(service); + Self::reinitialize_service(name, managed).await; + break; + } + Err(e) => { + error!(service = %name, error = %e, "Service dispatch failed"); + guard.clear_ready(); + drop(service); + Self::reinitialize_service(name, managed).await; + break; + } + } + } + } + + /// Dispatch events for service without file descriptor (polling) + async fn dispatch_polling(name: &str, managed: &Arc) { + let dispatch_interval = managed.service.lock().await.dispatch_interval(); + let mut interval_timer = interval(dispatch_interval); + interval_timer.set_missed_tick_behavior(MissedTickBehavior::Skip); + + loop { + interval_timer.tick().await; + + // Check if still running + if *managed.state.read() != ServiceState::Running { + break; + } + + let mut service = managed.service.lock().await; + + match service.dispatch().await { + Ok(DispatchAction::Continue) => {} + Ok(DispatchAction::Reinitialize) => { + info!(service = %name, "Service requested reinitialization"); + drop(service); + Self::reinitialize_service(name, managed).await; + break; + } + Err(e) => { + error!(service = %name, error = %e, "Service dispatch failed"); + drop(service); + Self::reinitialize_service(name, managed).await; + break; + } + } + } + } + + /// Reinitialize a service (finalize, then mark for retry) + async fn reinitialize_service(name: &str, managed: &Arc) { + debug!(service = %name, "Reinitializing service"); + + let mut service = managed.service.lock().await; + + if let Err(e) = service.finalize().await { + warn!(service = %name, error = %e, "Error finalizing service"); + } + + drop(service); + + // Clear async fd and mark for retry + *managed.async_fd.write() = None; + *managed.state.write() = ServiceState::Uninitialized; + *managed.error_count.write() = 0; + } + + /// Shutdown all services gracefully + async fn shutdown_all_services_static(services: &HashMap>) { + for (name, managed) in services { + if *managed.state.read() != ServiceState::Running { + continue; + } + + info!(service = %name, "Shutting down service"); + + let mut service = managed.service.lock().await; + + if let Err(e) = service.finalize().await { + error!(service = %name, error = %e, "Error finalizing service"); + } + } + } +} + +impl Default for ServiceManager { + fn default() -> Self { + Self::new() + } +} diff --git a/src/pmxcfs-rs/pmxcfs-services/src/service.rs b/src/pmxcfs-rs/pmxcfs-services/src/service.rs new file mode 100644 index 00000000..395ba67f --- /dev/null +++ b/src/pmxcfs-rs/pmxcfs-services/src/service.rs @@ -0,0 +1,173 @@ +//! Service trait and related types +//! +//! This module provides the core abstraction for managed services that can +//! automatically retry initialization, handle errors gracefully, and provide +//! timer-based periodic callbacks. + +use crate::error::Result; +use async_trait::async_trait; +use std::time::Duration; + +/// A managed service that can be monitored and restarted automatically +/// +/// This trait provides the core abstraction for services in the pmxcfs daemon. +/// Services implementing this trait gain automatic retry on failure, graceful +/// error handling, and optional periodic timer callbacks. +/// +/// ## Lifecycle +/// +/// 1. **Uninitialized** - Service created but not yet initialized +/// 2. **Initializing** - `initialize()` in progress +/// 3. **Running** - Service initialized successfully, dispatching events +/// 4. **Failed** - Service encountered an error, will retry if restartable +#[async_trait] +pub trait Service: Send + Sync { + /// Service name for logging and identification + /// + /// Should be a short, descriptive identifier (e.g., "quorum", "dfsm", "confdb") + fn name(&self) -> &str; + + /// Initialize the service + /// + /// Called when the service is first started or after a failure (if restartable). + /// Returns an `InitResult` indicating whether the service needs file descriptor + /// monitoring. + /// + /// # Errors + /// + /// Returns an error if initialization fails. The ServiceManager will automatically + /// retry initialization based on `retry_interval()` if `is_restartable()` returns true. + /// + /// # Implementation Notes + /// + /// - Initialize connections to external services (Corosync, CPG, etc.) + /// - Set up internal state + /// - Return file descriptor if the service needs event-driven dispatching + /// - Keep initialization lightweight - heavy work should be in `dispatch()` + async fn initialize(&mut self) -> Result; + + /// Handle events for this service + /// + /// Called when: + /// - The file descriptor returned by `initialize()` becomes readable (if WithFileDescriptor) + /// - Periodically for services without file descriptors (if NoFileDescriptor) + /// + /// # Returns + /// + /// - `DispatchAction::Continue` - Continue normal operation + /// - `DispatchAction::Reinitialize` - Request reinitialization (triggers `finalize()` then `initialize()`) + /// + /// # Errors + /// + /// Errors automatically trigger reinitialization if the service is restartable. + /// The service will be finalized and reinitialized according to `retry_interval()`. + async fn dispatch(&mut self) -> Result; + + /// Clean up service resources + /// + /// Called when: + /// - Service is being shut down + /// - Service is being reinitialized after dispatch failure + /// - ServiceManager is shutting down + /// + /// # Implementation Notes + /// + /// - Close connections + /// - Release resources + /// - Should not fail - log errors but return Ok(()) + async fn finalize(&mut self) -> Result<()>; + + /// Optional periodic callback + /// + /// Called at the interval specified by `timer_period()` if the service is running. + /// Useful for periodic maintenance tasks like state verification or cleanup. + /// + /// # Default Implementation + /// + /// Does nothing by default. Override to implement periodic behavior. + async fn timer_callback(&mut self) -> Result<()> { + Ok(()) + } + + /// Timer period for periodic callbacks + /// + /// If `Some(duration)`, `timer_callback()` will be invoked every `duration`. + /// If `None`, timer callbacks are disabled. + /// + /// # Default + /// + /// Returns `None` (no timer callbacks) + fn timer_period(&self) -> Option { + None + } + + /// Whether to automatically retry initialization after failure + /// + /// If `true`, the ServiceManager will automatically retry `initialize()` + /// after failures using the interval specified by `retry_interval()`. + /// + /// If `false`, the service will remain in a failed state after the first + /// initialization failure. + /// + /// # Default + /// + /// Returns `true` (auto-retry enabled) + fn is_restartable(&self) -> bool { + true + } + + /// Minimum interval between retry attempts + /// + /// When `initialize()` fails, the ServiceManager will wait at least this + /// long before attempting to reinitialize. + /// + /// # Default + /// + /// Returns 5 seconds (matching C implementation) + fn retry_interval(&self) -> Duration { + Duration::from_secs(5) + } + + /// Dispatch interval for services without file descriptors + /// + /// For services that return `InitResult::NoFileDescriptor`, this determines + /// how often `dispatch()` is called. + /// + /// # Default + /// + /// Returns 100ms (matching current Rust implementation) + fn dispatch_interval(&self) -> Duration { + Duration::from_millis(100) + } +} + +/// Result of service initialization +#[derive(Debug, Clone, Copy)] +pub enum InitResult { + /// Service uses a file descriptor for event notification + /// + /// The ServiceManager will use tokio's AsyncFd to monitor this file descriptor + /// and call `dispatch()` when it becomes readable. This is the most efficient + /// mode for services that interact with Corosync (quorum, CPG, cmap). + WithFileDescriptor(i32), + + /// Service does not use a file descriptor + /// + /// The ServiceManager will call `dispatch()` periodically at the interval + /// specified by `dispatch_interval()`. Use this for services that poll + /// or have no external event source. + NoFileDescriptor, +} + +/// Action requested by service dispatch +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DispatchAction { + /// Continue normal operation + Continue, + + /// Request reinitialization + /// + /// The service will be finalized and reinitialized. This is useful when + /// the underlying connection is lost or becomes invalid. + Reinitialize, +} diff --git a/src/pmxcfs-rs/pmxcfs-services/tests/service_tests.rs b/src/pmxcfs-rs/pmxcfs-services/tests/service_tests.rs new file mode 100644 index 00000000..4574a8d6 --- /dev/null +++ b/src/pmxcfs-rs/pmxcfs-services/tests/service_tests.rs @@ -0,0 +1,808 @@ +//! Comprehensive tests for the service framework +//! +//! Tests cover: +//! - Service lifecycle (start, stop, restart) +//! - Service manager orchestration +//! - Error handling and retry logic +//! - Timer callbacks +//! - File descriptor and polling dispatch modes +//! - Service coordination and state management + +use async_trait::async_trait; +use pmxcfs_services::{DispatchAction, InitResult, Service, ServiceError, ServiceManager}; +use pmxcfs_test_utils::wait_for_condition; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::time::Duration; +use tokio::time::sleep; + +// ===== Test Service Implementations ===== + +/// Mock service for testing lifecycle +struct MockService { + name: String, + init_count: Arc, + dispatch_count: Arc, + finalize_count: Arc, + timer_count: Arc, + should_fail_init: Arc, + should_fail_dispatch: Arc, + should_reinit: Arc, + use_fd: bool, + timer_period: Option, + restartable: bool, +} + +impl MockService { + fn new(name: &str) -> Self { + Self { + name: name.to_string(), + init_count: Arc::new(AtomicU32::new(0)), + dispatch_count: Arc::new(AtomicU32::new(0)), + finalize_count: Arc::new(AtomicU32::new(0)), + timer_count: Arc::new(AtomicU32::new(0)), + should_fail_init: Arc::new(AtomicBool::new(false)), + should_fail_dispatch: Arc::new(AtomicBool::new(false)), + should_reinit: Arc::new(AtomicBool::new(false)), + use_fd: false, + timer_period: None, + restartable: true, + } + } + + fn with_timer(mut self, period: Duration) -> Self { + self.timer_period = Some(period); + self + } + + fn with_restartable(mut self, restartable: bool) -> Self { + self.restartable = restartable; + self + } + + fn counters(&self) -> ServiceCounters { + ServiceCounters { + init_count: self.init_count.clone(), + dispatch_count: self.dispatch_count.clone(), + finalize_count: self.finalize_count.clone(), + timer_count: self.timer_count.clone(), + should_fail_init: self.should_fail_init.clone(), + should_fail_dispatch: self.should_fail_dispatch.clone(), + should_reinit: self.should_reinit.clone(), + } + } +} + +#[async_trait] +impl Service for MockService { + fn name(&self) -> &str { + &self.name + } + + async fn initialize(&mut self) -> pmxcfs_services::Result { + self.init_count.fetch_add(1, Ordering::SeqCst); + + if self.should_fail_init.load(Ordering::SeqCst) { + return Err(ServiceError::InitializationFailed( + "Mock init failure".to_string(), + )); + } + + if self.use_fd { + // Return a dummy fd (stderr is always available) + Ok(InitResult::WithFileDescriptor(2)) + } else { + Ok(InitResult::NoFileDescriptor) + } + } + + async fn dispatch(&mut self) -> pmxcfs_services::Result { + self.dispatch_count.fetch_add(1, Ordering::SeqCst); + + if self.should_fail_dispatch.load(Ordering::SeqCst) { + return Err(ServiceError::DispatchFailed( + "Mock dispatch failure".to_string(), + )); + } + + if self.should_reinit.load(Ordering::SeqCst) { + return Ok(DispatchAction::Reinitialize); + } + + Ok(DispatchAction::Continue) + } + + async fn finalize(&mut self) -> pmxcfs_services::Result<()> { + self.finalize_count.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + async fn timer_callback(&mut self) -> pmxcfs_services::Result<()> { + self.timer_count.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + fn timer_period(&self) -> Option { + self.timer_period + } + + fn is_restartable(&self) -> bool { + self.restartable + } + + fn retry_interval(&self) -> Duration { + Duration::from_millis(100) // Fast retry for tests + } + + fn dispatch_interval(&self) -> Duration { + Duration::from_millis(50) // Fast polling for tests + } +} + +/// Helper struct to access service counters from tests +#[derive(Clone)] +struct ServiceCounters { + init_count: Arc, + dispatch_count: Arc, + finalize_count: Arc, + timer_count: Arc, + should_fail_init: Arc, + should_fail_dispatch: Arc, + should_reinit: Arc, +} + +impl ServiceCounters { + fn init_count(&self) -> u32 { + self.init_count.load(Ordering::SeqCst) + } + + fn dispatch_count(&self) -> u32 { + self.dispatch_count.load(Ordering::SeqCst) + } + + fn finalize_count(&self) -> u32 { + self.finalize_count.load(Ordering::SeqCst) + } + + fn timer_count(&self) -> u32 { + self.timer_count.load(Ordering::SeqCst) + } + + fn set_fail_init(&self, fail: bool) { + self.should_fail_init.store(fail, Ordering::SeqCst); + } + + fn set_fail_dispatch(&self, fail: bool) { + self.should_fail_dispatch.store(fail, Ordering::SeqCst); + } + + fn set_reinit(&self, reinit: bool) { + self.should_reinit.store(reinit, Ordering::SeqCst); + } +} + +// ===== Lifecycle Tests ===== + +#[tokio::test] +async fn test_service_lifecycle_basic() { + let service = MockService::new("test_service"); + let counters = service.counters(); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service)); + + let shutdown_token = manager.shutdown_token(); + let handle = manager.spawn(); + + // Wait for initialization and dispatching + assert!( + wait_for_condition( + || counters.init_count() >= 1 && counters.dispatch_count() >= 1, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Service should initialize and dispatch within 5 seconds" + ); + + // Shutdown + shutdown_token.cancel(); + let _ = handle.await; + + // Service should be finalized + assert_eq!( + counters.finalize_count(), + 1, + "Service should be finalized exactly once" + ); +} + +#[tokio::test] +async fn test_service_with_file_descriptor() { + // Don't use FD-based service in tests since we can't easily create a readable FD + // Just test that WithFileDescriptor variant works with manager + let service = MockService::new("no_fd_service"); // Changed to not use FD + let counters = service.counters(); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service)); + + let shutdown_token = manager.shutdown_token(); + let handle = manager.spawn(); + + // Wait for initialization and some dispatches + assert!( + wait_for_condition( + || counters.init_count() == 1 && counters.dispatch_count() >= 1, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Service should initialize once and dispatch within 5 seconds" + ); + + // Shutdown + shutdown_token.cancel(); + let _ = handle.await; + + assert_eq!(counters.finalize_count(), 1, "Service should finalize once"); +} + +#[tokio::test] +async fn test_service_initialization_failure() { + let service = MockService::new("failing_service"); + let counters = service.counters(); + + // Make initialization fail + counters.set_fail_init(true); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service)); + + let shutdown_token = manager.shutdown_token(); + let handle = manager.spawn(); + + // Wait for several retry attempts + assert!( + wait_for_condition( + || counters.init_count() >= 3, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Service should retry initialization at least 3 times within 5 seconds" + ); + + // Dispatch should not run if init fails + assert_eq!( + counters.dispatch_count(), + 0, + "Service should not dispatch if init fails" + ); + + // Shutdown + shutdown_token.cancel(); + let _ = handle.await; +} + +#[tokio::test] +async fn test_service_initialization_recovery() { + let service = MockService::new("recovering_service"); + let counters = service.counters(); + + // Start with failing initialization + counters.set_fail_init(true); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service)); + + let shutdown_token = manager.shutdown_token(); + let handle = manager.spawn(); + + // Wait for some failed attempts + assert!( + wait_for_condition( + || counters.init_count() >= 2, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Should have at least 2 failed initialization attempts within 5 seconds" + ); + + let failed_attempts = counters.init_count(); + + // Allow initialization to succeed + counters.set_fail_init(false); + + // Wait for recovery + assert!( + wait_for_condition( + || counters.init_count() > failed_attempts && counters.dispatch_count() >= 1, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Service should recover and start dispatching within 5 seconds" + ); + + // Shutdown + shutdown_token.cancel(); + let _ = handle.await; +} + +#[tokio::test] +async fn test_service_not_restartable() { + let service = MockService::new("non_restartable").with_restartable(false); + let counters = service.counters(); + + // Make initialization fail + counters.set_fail_init(true); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service)); + + let shutdown_token = manager.shutdown_token(); + let handle = manager.spawn(); + + // Wait for initialization attempt + assert!( + wait_for_condition( + || counters.init_count() >= 1, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Service should attempt initialization within 5 seconds" + ); + + // Service should only try once (not restartable) + assert_eq!( + counters.init_count(), + 1, + "Non-restartable service should only try initialization once" + ); + + // Wait another cycle to confirm it doesn't retry + sleep(Duration::from_millis(1500)).await; + + // Should still be 1 + assert_eq!( + counters.init_count(), + 1, + "Non-restartable service should not retry, got {}", + counters.init_count() + ); + + // Shutdown + shutdown_token.cancel(); + let _ = handle.await; +} + +// ===== Dispatch Tests ===== + +#[tokio::test] +async fn test_service_dispatch_failure_triggers_reinit() { + let service = MockService::new("dispatch_fail_service"); + let counters = service.counters(); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service)); + + let shutdown_token = manager.shutdown_token(); + let handle = manager.spawn(); + + // Wait for initialization and first dispatches + assert!( + wait_for_condition( + || counters.init_count() == 1 && counters.dispatch_count() >= 1, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Service should initialize once and dispatch within 5 seconds" + ); + + // Make dispatch fail + counters.set_fail_dispatch(true); + + // Wait for dispatch failure and reinitialization + assert!( + wait_for_condition( + || counters.init_count() >= 2 && counters.finalize_count() >= 1, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Service should reinitialize and finalize after dispatch failure within 5 seconds" + ); + + // Shutdown + shutdown_token.cancel(); + let _ = handle.await; +} + +#[tokio::test] +async fn test_service_dispatch_requests_reinit() { + let service = MockService::new("reinit_request_service"); + let counters = service.counters(); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service)); + + let shutdown_token = manager.shutdown_token(); + let handle = manager.spawn(); + + // Wait for initialization + assert!( + wait_for_condition( + || counters.init_count() == 1, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Service should initialize once within 5 seconds" + ); + + // Request reinitialization from dispatch + counters.set_reinit(true); + + // Wait for reinitialization + assert!( + wait_for_condition( + || counters.init_count() >= 2 && counters.finalize_count() >= 1, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Service should reinitialize and finalize when dispatch requests it within 5 seconds" + ); + + // Shutdown + shutdown_token.cancel(); + let _ = handle.await; +} + +// ===== Timer Callback Tests ===== + +#[tokio::test] +async fn test_service_timer_callback() { + let service = MockService::new("timer_service").with_timer(Duration::from_millis(300)); + let counters = service.counters(); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service)); + + let shutdown_token = manager.shutdown_token(); + let handle = manager.spawn(); + + // Wait for initialization plus several timer periods + assert!( + wait_for_condition( + || counters.timer_count() >= 3, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Timer should fire at least 3 times within 5 seconds" + ); + + let timer_count = counters.timer_count(); + + // Wait for more timer invocations + assert!( + wait_for_condition( + || counters.timer_count() > timer_count, + Duration::from_secs(2), + Duration::from_millis(10), + ) + .await, + "Timer should continue firing" + ); + + // Shutdown + shutdown_token.cancel(); + let _ = handle.await; +} + +#[tokio::test] +async fn test_service_timer_callback_not_invoked_when_failed() { + let service = MockService::new("failed_timer_service").with_timer(Duration::from_millis(100)); + let counters = service.counters(); + + // Make initialization fail + counters.set_fail_init(true); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service)); + + let shutdown_token = manager.shutdown_token(); + let handle = manager.spawn(); + + // Wait for several timer periods + sleep(Duration::from_millis(2000)).await; + + // Timer should NOT fire if service is not running + assert_eq!( + counters.timer_count(), + 0, + "Timer should not fire when service is not running" + ); + + // Shutdown + shutdown_token.cancel(); + let _ = handle.await; +} + +// ===== Service Manager Tests ===== + +#[tokio::test] +async fn test_manager_multiple_services() { + let service1 = MockService::new("service1"); + let service2 = MockService::new("service2"); + let service3 = MockService::new("service3"); + + let counters1 = service1.counters(); + let counters2 = service2.counters(); + let counters3 = service3.counters(); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service1)); + manager.add_service(Box::new(service2)); + manager.add_service(Box::new(service3)); + + let shutdown_token = manager.shutdown_token(); + let handle = manager.spawn(); + + // Wait for initialization + assert!( + wait_for_condition( + || counters1.init_count() == 1 + && counters2.init_count() == 1 + && counters3.init_count() == 1 + && counters1.dispatch_count() >= 1 + && counters2.dispatch_count() >= 1 + && counters3.dispatch_count() >= 1, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "All services should initialize and dispatch within 5 seconds" + ); + + // Shutdown + shutdown_token.cancel(); + let _ = handle.await; + + // All services should be finalized + assert_eq!(counters1.finalize_count(), 1, "Service1 should finalize"); + assert_eq!(counters2.finalize_count(), 1, "Service2 should finalize"); + assert_eq!(counters3.finalize_count(), 1, "Service3 should finalize"); +} + +#[tokio::test] +#[should_panic(expected = "already registered")] +async fn test_manager_duplicate_service_name() { + let service1 = MockService::new("duplicate"); + let service2 = MockService::new("duplicate"); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service1)); + manager.add_service(Box::new(service2)); // Should panic +} + +#[tokio::test] +async fn test_manager_partial_service_failure() { + let service1 = MockService::new("working_service"); + let service2 = MockService::new("failing_service"); + + let counters1 = service1.counters(); + let counters2 = service2.counters(); + + // Make service2 fail + counters2.set_fail_init(true); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service1)); + manager.add_service(Box::new(service2)); + + let shutdown_token = manager.shutdown_token(); + let handle = manager.spawn(); + + // Wait for initialization + assert!( + wait_for_condition( + || counters1.init_count() == 1 + && counters1.dispatch_count() >= 1 + && counters2.init_count() >= 2, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Service1 should work normally and Service2 should retry within 5 seconds" + ); + + // Service2 should not dispatch when failing + assert_eq!( + counters2.dispatch_count(), + 0, + "Service2 should not dispatch when failing" + ); + + // Shutdown + shutdown_token.cancel(); + let _ = handle.await; + + // Only service1 should finalize (service2 never initialized) + assert_eq!(counters1.finalize_count(), 1, "Service1 should finalize"); + assert_eq!( + counters2.finalize_count(), + 0, + "Service2 should not finalize if never initialized" + ); +} + +// ===== Error Handling Tests ===== + +#[tokio::test] +async fn test_service_error_count_tracking() { + let service = MockService::new("error_tracking_service"); + let counters = service.counters(); + + // Make initialization fail + counters.set_fail_init(true); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service)); + + let shutdown_token = manager.shutdown_token(); + let handle = manager.spawn(); + + // Wait for multiple failures + assert!( + wait_for_condition( + || counters.init_count() >= 4, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Should accumulate at least 4 failures within 5 seconds" + ); + + // Allow recovery + counters.set_fail_init(false); + + // Wait for recovery + assert!( + wait_for_condition( + || counters.dispatch_count() >= 1, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Service should recover within 5 seconds" + ); + + // Shutdown + shutdown_token.cancel(); + let _ = handle.await; +} + +#[tokio::test] +async fn test_service_graceful_shutdown() { + let service = MockService::new("shutdown_test"); + let counters = service.counters(); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service)); + + let shutdown_token = manager.shutdown_token(); + let handle = manager.spawn(); + + // Wait for service to be running + assert!( + wait_for_condition( + || counters.dispatch_count() >= 1, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Service should be running within 5 seconds" + ); + + // Graceful shutdown + shutdown_token.cancel(); + let _ = handle.await; + + // Service should be properly finalized + assert_eq!( + counters.finalize_count(), + 1, + "Service should finalize during shutdown" + ); +} + +// ===== Concurrency Tests ===== + +#[tokio::test] +async fn test_service_concurrent_operations() { + let service = MockService::new("concurrent_service").with_timer(Duration::from_millis(200)); + let counters = service.counters(); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service)); + + let shutdown_token = manager.shutdown_token(); + let handle = manager.spawn(); + + // Wait for service to run with both dispatch and timer + assert!( + wait_for_condition( + || counters.dispatch_count() >= 3 && counters.timer_count() >= 3, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Service should dispatch and timer should fire multiple times within 5 seconds" + ); + + // Shutdown + shutdown_token.cancel(); + let _ = handle.await; +} + +#[tokio::test] +async fn test_service_state_consistency_after_reinit() { + let service = MockService::new("consistency_service"); + let counters = service.counters(); + + let mut manager = ServiceManager::new(); + manager.add_service(Box::new(service)); + + let shutdown_token = manager.shutdown_token(); + let handle = manager.spawn(); + + // Wait for initialization + assert!( + wait_for_condition( + || counters.init_count() >= 1, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Service should initialize within 5 seconds" + ); + + // Trigger reinitialization + counters.set_reinit(true); + + // Wait for reinit + assert!( + wait_for_condition( + || counters.init_count() >= 2, + Duration::from_secs(5), + Duration::from_millis(10), + ) + .await, + "Service should reinitialize within 5 seconds" + ); + + // Clear reinit flag + counters.set_reinit(false); + + // Wait for more dispatches + let dispatch_count = counters.dispatch_count(); + assert!( + wait_for_condition( + || counters.dispatch_count() > dispatch_count, + Duration::from_secs(2), + Duration::from_millis(10), + ) + .await, + "Service should continue dispatching after reinit" + ); + + // Shutdown + shutdown_token.cancel(); + let _ = handle.await; +} -- 2.47.3 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel