diff --git a/.busbar-ref b/.busbar-ref index 3e77816..ef2d95b 100644 --- a/.busbar-ref +++ b/.busbar-ref @@ -1 +1 @@ -e1d3d8b097201859751d77208e66249d98b50a4b 1.6.0 +bf32f11ba9634c77afbe1dc89a8c3986499b38af 1.6.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 023dd3c..03b2072 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ permissions: jobs: ci: - uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@e1d3d8b097201859751d77208e66249d98b50a4b + uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@bf32f11ba9634c77afbe1dc89a8c3986499b38af with: service: none busbar_checkout: true diff --git a/.github/workflows/consumer-verify.yml b/.github/workflows/consumer-verify.yml index 53cf732..50d5b0d 100644 --- a/.github/workflows/consumer-verify.yml +++ b/.github/workflows/consumer-verify.yml @@ -21,7 +21,7 @@ permissions: jobs: consumer: - uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@e1d3d8b097201859751d77208e66249d98b50a4b + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@bf32f11ba9634c77afbe1dc89a8c3986499b38af with: version: ${{ inputs.version || '' }} asset_prefix: busbar-store-sqlite diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f35de79..3113f8b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ permissions: jobs: ci: - uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@e1d3d8b097201859751d77208e66249d98b50a4b + uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@bf32f11ba9634c77afbe1dc89a8c3986499b38af with: service: none busbar_checkout: true @@ -30,7 +30,7 @@ jobs: secrets: inherit release: needs: ci - uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@e1d3d8b097201859751d77208e66249d98b50a4b + uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@bf32f11ba9634c77afbe1dc89a8c3986499b38af with: plugin_crate: busbar-store-sqlite-plugin manifest_name: busbar-store-sqlite @@ -46,7 +46,7 @@ jobs: consumer-verify: needs: release if: ${{ !cancelled() }} - uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@e1d3d8b097201859751d77208e66249d98b50a4b + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@bf32f11ba9634c77afbe1dc89a8c3986499b38af with: version: ${{ github.ref_name }} asset_prefix: busbar-store-sqlite diff --git a/.github/workflows/repin.yml b/.github/workflows/repin.yml index ef6ea15..16b7456 100644 --- a/.github/workflows/repin.yml +++ b/.github/workflows/repin.yml @@ -28,7 +28,7 @@ permissions: jobs: repin: - uses: GetBusbar/busbar/.github/workflows/plugin-repin.yml@e1d3d8b097201859751d77208e66249d98b50a4b + uses: GetBusbar/busbar/.github/workflows/plugin-repin.yml@bf32f11ba9634c77afbe1dc89a8c3986499b38af with: busbar_sha: ${{ github.event.client_payload.sha || inputs.busbar_sha }} busbar_version: ${{ github.event.client_payload.version || inputs.busbar_version }} diff --git a/Cargo.lock b/Cargo.lock index e3c42eb..f4bcbde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", @@ -39,9 +39,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -52,15 +52,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-buffer" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" -dependencies = [ - "hybrid-array", -] - [[package]] name = "bumpalo" version = "3.20.3" @@ -70,9 +61,10 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "busbar-contract" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=e1d3d8b097201859751d77208e66249d98b50a4b#e1d3d8b097201859751d77208e66249d98b50a4b" +source = "git+https://github.com/GetBusbar/busbar?rev=bf32f11ba9634c77afbe1dc89a8c3986499b38af#bf32f11ba9634c77afbe1dc89a8c3986499b38af" dependencies = [ "async-trait", + "base64", "futures", "hex", "http", @@ -81,7 +73,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml_ng", - "sha2 0.11.0", + "sha2", "smallvec", "tracing", "tracing-core", @@ -92,28 +84,27 @@ dependencies = [ [[package]] name = "busbar-kernel-ledger" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=e1d3d8b097201859751d77208e66249d98b50a4b#e1d3d8b097201859751d77208e66249d98b50a4b" +source = "git+https://github.com/GetBusbar/busbar?rev=bf32f11ba9634c77afbe1dc89a8c3986499b38af#bf32f11ba9634c77afbe1dc89a8c3986499b38af" dependencies = [ "busbar-contract", + "ring", "serde", - "sha2 0.11.0", ] [[package]] name = "busbar-kernel-wal" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=e1d3d8b097201859751d77208e66249d98b50a4b#e1d3d8b097201859751d77208e66249d98b50a4b" +source = "git+https://github.com/GetBusbar/busbar?rev=bf32f11ba9634c77afbe1dc89a8c3986499b38af#bf32f11ba9634c77afbe1dc89a8c3986499b38af" dependencies = [ "busbar-contract", - "sha2 0.11.0", + "ring", ] [[package]] name = "busbar-plugin-loader" version = "1.5.0" -source = "git+https://github.com/GetBusbar/busbar?rev=e1d3d8b097201859751d77208e66249d98b50a4b#e1d3d8b097201859751d77208e66249d98b50a4b" +source = "git+https://github.com/GetBusbar/busbar?rev=bf32f11ba9634c77afbe1dc89a8c3986499b38af#bf32f11ba9634c77afbe1dc89a8c3986499b38af" dependencies = [ - "async-trait", "busbar-contract", "busbar-kernel-ledger", "busbar-kernel-wal", @@ -126,7 +117,6 @@ dependencies = [ "libloading", "serde", "serde_json", - "sha2 0.11.0", "tar", "tokio", "tracing", @@ -164,9 +154,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.4.0" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "shlex", @@ -180,15 +170,15 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -201,12 +191,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "const-oid" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" - [[package]] name = "cpufeatures" version = "0.2.17" @@ -227,9 +211,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] @@ -244,15 +228,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-common" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" -dependencies = [ - "hybrid-array", -] - [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -262,7 +237,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest 0.10.7", + "digest", "fiat-crypto", "rustc_version", "subtle", @@ -277,7 +252,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.117", ] [[package]] @@ -286,7 +261,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid 0.9.6", + "const-oid", "zeroize", ] @@ -296,30 +271,8 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", -] - -[[package]] -name = "digest" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" -dependencies = [ - "block-buffer 0.12.1", - "const-oid 0.10.2", - "crypto-common 0.2.2", -] - -[[package]] -name = "displaydoc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", + "block-buffer", + "crypto-common", ] [[package]] @@ -341,7 +294,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2 0.10.9", + "sha2", "subtle", "zeroize", ] @@ -352,16 +305,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "fallible-iterator" version = "0.3.0" @@ -398,9 +341,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", "miniz_oxide", @@ -578,9 +521,9 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" dependencies = [ "hashbrown 0.17.1", ] @@ -593,9 +536,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" -version = "1.5.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -613,9 +556,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -630,20 +573,11 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "hybrid-array" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" -dependencies = [ - "typenum", -] - [[package]] name = "hyper" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -698,88 +632,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - [[package]] name = "idna" version = "1.1.0" @@ -793,12 +645,22 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +checksum = "279259b0ac81c89d11c290495fdcfa96ea3643b7df311c138b6fe8ca5237f0f8" dependencies = [ - "icu_normalizer", - "icu_properties", + "idna_mapping", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna_mapping" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11c13906586a4b339310541a274dd927aff6fcbb5b8e3af90634c4b31681c792" +dependencies = [ + "unicode-joining-type", ] [[package]] @@ -827,12 +689,13 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", + "once_cell", "wasm-bindgen", ] @@ -863,23 +726,11 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - [[package]] name = "log" -version = "0.4.33" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lru-slab" @@ -889,15 +740,15 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "memchr" -version = "2.8.3" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", @@ -905,9 +756,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -944,33 +795,24 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "potential_utf" -version = "0.1.5" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "proc-macro2" -version = "1.0.107" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quinn" -version = "0.11.11" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -1010,9 +852,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.15" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", @@ -1024,9 +866,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.47" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1159,9 +1001,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.3" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -1172,24 +1014,11 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] - [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "once_cell", "ring", @@ -1211,9 +1040,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -1222,9 +1051,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.23" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -1314,25 +1143,14 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", + "digest", ] [[package]] name = "shlex" -version = "2.0.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signature" @@ -1357,15 +1175,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" [[package]] name = "socket2" -version = "0.6.5" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -1393,12 +1211,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - [[package]] name = "subtle" version = "2.6.1" @@ -1407,9 +1219,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.119" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1436,17 +1248,6 @@ dependencies = [ "futures-core", ] -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "tar" version = "0.4.46" @@ -1455,44 +1256,33 @@ checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", - "xattr", ] [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", + "syn 2.0.117", ] [[package]] name = "tinyvec" -version = "1.12.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -1519,9 +1309,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" dependencies = [ "rustls", "tokio", @@ -1591,7 +1381,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.117", ] [[package]] @@ -1627,12 +1417,33 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-joining-type" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d00a78170970967fdb83f9d49b92f959ab2bb829186b113e4f4604ad98e180" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -1698,18 +1509,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.4+wasi-0.2.12" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -1720,9 +1531,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.76" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -1730,9 +1541,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1740,31 +1551,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -1782,9 +1593,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.9" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -1883,107 +1694,14 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix", -] - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - [[package]] name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "zmij" -version = "1.0.23" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 98169bb..09ee4d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,5 +23,5 @@ publish = false repository = "https://github.com/GetBusbar/busbar-store-sqlite" [workspace.dependencies] -busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "e1d3d8b097201859751d77208e66249d98b50a4b" } -busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "e1d3d8b097201859751d77208e66249d98b50a4b" } +busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "bf32f11ba9634c77afbe1dc89a8c3986499b38af" } +busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "bf32f11ba9634c77afbe1dc89a8c3986499b38af" } diff --git a/README.md b/README.md index a17b640..ac07927 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,7 @@ its one door (`door::door`, the store v3 table over `SqliteStore` through into a `SqliteStore`), and the plugin crate exports that door as `busbar_plugin_door` (`export_door!`), so the cdylib answers the loader through the same door a busbar build that LINKS `busbar-store-sqlite` -registers — one source, both doors. The plugin crate also registers the store -on the cold store lane (`export_store_plugin!`) that the busbar kernel at the -pin boots a configured `store:` through. +registers — one source, both doors. The image carries no other export. - The **default durable store** for busbar's governance data: virtual diff --git a/store-sqlite-plugin/Cargo.toml b/store-sqlite-plugin/Cargo.toml index 8894521..b06a9b3 100644 --- a/store-sqlite-plugin/Cargo.toml +++ b/store-sqlite-plugin/Cargo.toml @@ -16,13 +16,13 @@ crate-type = ["cdylib", "rlib"] [dependencies] # THE LOGIC, same repo: its `door::door` is the door this image exports. busbar-store-sqlite = { path = "../store-sqlite" } -busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "e1d3d8b097201859751d77208e66249d98b50a4b" } +busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "bf32f11ba9634c77afbe1dc89a8c3986499b38af" } [dev-dependencies] # The conformance and e2e tests load the built cdylib and the linked door over the REAL loader (the # one dispatcher, the store v3 table) — dev-only, never in the shipped artifact. Same pinned rev as # the logic crate's busbar-contract, so one busbar source resolves. -busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "e1d3d8b097201859751d77208e66249d98b50a4b" } +busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "bf32f11ba9634c77afbe1dc89a8c3986499b38af" } serde_json = "1" # Dev-only: the conformance test awaits the store v3 calls (`StoreCalls`), which are futures. tokio = { version = "1", features = ["rt"] } diff --git a/store-sqlite-plugin/src/lib.rs b/store-sqlite-plugin/src/lib.rs index d576fd8..35097a8 100644 --- a/store-sqlite-plugin/src/lib.rs +++ b/store-sqlite-plugin/src/lib.rs @@ -9,12 +9,11 @@ //! (`busbar_store_sqlite::door::door`, the store v3 table). This crate re-exports the logic crate, //! so the library it builds carries exactly the code a busbar build links, and exports that door as //! `busbar_plugin_door` (`export_door!`, unconditionally) — one source, both doors (DECISIONS #2 -//! rule (1)). It also registers the store on the cold store lane the busbar kernel at the pin boots -//! a configured store through (`cold`, `export_store_plugin!`). +//! rule (1)). The image carries no other export: the memory-ABI door is the only way a busbar +//! reaches this store. //! -//! This crate is `deny`, not `forbid`: the two export macros (`#[unsafe(no_mangle)]`, the cold -//! boundary's `unsafe extern` functions) are the reviewed exemptions (a `forbid` cannot be lifted -//! for them). No other `unsafe` exists here. +//! This crate is `deny`, not `forbid`: the export macro (`#[unsafe(no_mangle)]`) is the reviewed +//! exemption (a `forbid` cannot be lifted for it). No other `unsafe` exists here. #![deny(unsafe_code)] @@ -25,21 +24,3 @@ pub use busbar_store_sqlite::*; mod exported { busbar_contract::export_door!(busbar_store_sqlite::door::door); } - -/// The store a busbar at the pin BOOTS. Its kernel opens a configured dropped-in store through the -/// cold store lane (`PluginRegistry::open_store` -> `load_store_image`: `busbar_abi`, -/// `busbar_plugin_kind`, `busbar_open`, ...), not through the door; an image that exports only -/// `busbar_plugin_door` answers `busbar_plugin_kind` with NULL there and the boot is refused -/// (BUSBAR-9007 "returned a null kind string"). This registration answers that lane over the same -/// [`SqliteStore`], opened by the door's own settings parser ([`door::open`]). -fn open_cold(cfg: &str) -> Result { - door::open(cfg).map(|s| Box::new(s) as busbar_contract::abi::sdk::StoreHandle) -} - -/// THE COLD LANE's registration (`export_store_plugin!`): the contract SDK's frozen symbols answer -/// through it. The macro's boundary functions are `unsafe extern "C-unwind"` by the cold ABI's own -/// definition, and it registers through a load-time initializer section — the other exemption. -#[allow(unsafe_code)] -mod cold { - busbar_contract::abi::sdk::export_store_plugin!(super::open_cold); -} diff --git a/store-sqlite-plugin/tests/conformance.rs b/store-sqlite-plugin/tests/conformance.rs index c3f3a98..7389252 100644 --- a/store-sqlite-plugin/tests/conformance.rs +++ b/store-sqlite-plugin/tests/conformance.rs @@ -16,7 +16,7 @@ //! cross-handle view), and a RESTART — every handle closed, the file reopened, everything read back //! and every `op_id` replayed. The two arms must agree on the whole transcript. //! -//! The RED arms are in the same test: (a) the door asked for as another kind is refused, linked and +//! The RED arms are their own tests (plugin-gates `bothways` needs a RED arm beside the both-ways test): (a) the door asked for as another kind is refused, linked and //! dropped in; (b) the dropped-in door opened on a file that already holds a foreign key yields a //! transcript that DIFFERS from the linked one — so the comparison above can see a real difference //! and is not vacuously equal. A missing cdylib PANICS: this test IS the dropped-in door's proof. @@ -92,11 +92,17 @@ fn load( } } +/// The node's one `op_id` allocator, as the kernel hands a store handle its own: the bridge's +/// additive writes mint from it. Its node half is one no test op id uses. +fn mint() -> busbar_contract::abi::store::OpId { + static N: AtomicU64 = AtomicU64::new(0); + busbar_contract::abi::store::OpId::from_parts(0x5e1f, N.fetch_add(1, Ordering::Relaxed) + 1) +} /// One store instance through `by`, opened on `cfg` as the host opens a store. fn open(by: Door, cfg: &str) -> Result { let d = Arc::new(Dispatcher::new(DispatchConfig::default())); let plugin = load::(by, &d).map_err(|e| e.to_string())?; - LoadedStore::open(plugin, d, cfg.as_bytes(), 1) + LoadedStore::open(plugin, d, cfg.as_bytes(), mint) } /// `close` the instance, as the host does at a restart: its connections to the file go with it. @@ -143,6 +149,31 @@ fn record(kind: &str, id: &str, parent: Option<&str>, seq: u64, body: &str) -> P } } +/// A reserve's answer as the transcript compares it: each grant's slice and amount. Its +/// `valid_until_ms` is the store's clock plus `SLICE_TTL_MS` (`abi::store::SLICE_TTL_MS` (c)), so it +/// differs from one run to the next; it is checked bounded here instead, never `u64::MAX`. +fn granted( + answer: Result, sc::StoreFailure>, +) -> String { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("a clock after 1970") + .as_millis() as u64; + match answer { + Ok(grants) => { + for g in &grants { + assert!( + g.valid_until_ms <= now + busbar_contract::abi::store::SLICE_TTL_MS, + "a durable store bounds a slice's validity: {g:?}" + ); + } + let cells: Vec<(u64, u64)> = grants.iter().map(|g| (g.slice_id, g.granted)).collect(); + format!("Ok({cells:?})") + } + Err(e) => format!("Err({e:?})"), + } +} + /// The slot the scenario draws from and caps. const SLOT: CellKey<'static> = CellKey { bucket: "vk_conf", @@ -211,7 +242,7 @@ fn v3_writes(s: &LoadedStore) -> Vec { "caps = {:?}", sc::StoreCalls::window_caps(s, op(1), &caps).await ), - format!("no cap = {:?}", { + format!("no cap = {}", { let other = [Cell { key: CellKey { bucket: "vk_other", @@ -219,23 +250,23 @@ fn v3_writes(s: &LoadedStore) -> Vec { }, amount: 1, }]; - sc::StoreCalls::reserve(s, op(2), 0, &other).await + granted(sc::StoreCalls::reserve(s, op(2), 0, &other).await) }), format!( - "reserve 6 = {:?}", - sc::StoreCalls::reserve(s, op(3), 0, &draw(6)).await + "reserve 6 = {}", + granted(sc::StoreCalls::reserve(s, op(3), 0, &draw(6)).await) ), format!( - "replay = {:?}", - sc::StoreCalls::reserve(s, op(3), 0, &draw(6)).await + "replay = {}", + granted(sc::StoreCalls::reserve(s, op(3), 0, &draw(6)).await) ), format!( - "conflict = {:?}", - sc::StoreCalls::reserve(s, op(3), 0, &draw(1)).await + "conflict = {}", + granted(sc::StoreCalls::reserve(s, op(3), 0, &draw(1)).await) ), format!( - "reserve 5 = {:?}", - sc::StoreCalls::reserve(s, op(4), 0, &draw(5)).await + "reserve 5 = {}", + granted(sc::StoreCalls::reserve(s, op(4), 0, &draw(5)).await) ), format!( "record_put = {:?}", @@ -264,24 +295,24 @@ fn v3_replays(s: &LoadedStore) -> Vec { block(async { vec![ format!( - "replay = {:?}", - sc::StoreCalls::reserve(s, op(3), 0, &draw(6)).await + "replay = {}", + granted(sc::StoreCalls::reserve(s, op(3), 0, &draw(6)).await) ), format!( - "conflict = {:?}", - sc::StoreCalls::reserve(s, op(3), 0, &draw(1)).await + "conflict = {}", + granted(sc::StoreCalls::reserve(s, op(3), 0, &draw(1)).await) ), format!( "append_batch replay = {:?}", sc::StoreCalls::append_batch(s, op(5), "journal", &[r(b"a"), r(b"b")]).await ), format!( - "reserve 4 = {:?}", - sc::StoreCalls::reserve(s, op(6), 0, &draw(4)).await + "reserve 4 = {}", + granted(sc::StoreCalls::reserve(s, op(6), 0, &draw(4)).await) ), format!( - "reserve 1 = {:?}", - sc::StoreCalls::reserve(s, op(7), 0, &draw(1)).await + "reserve 1 = {}", + granted(sc::StoreCalls::reserve(s, op(7), 0, &draw(1)).await) ), ] }) @@ -376,8 +407,8 @@ fn transcript(by: Door, tag: &str, seed: Option<&str>) -> serde_json::Value { }) } -/// The sqlite store behaves as ONE store through either door — and the RED arms show the -/// comparison can tell a different store apart. +/// The sqlite store behaves as ONE store through either door (the RED arms below show the +/// comparison can tell a different store apart). #[test] fn the_linked_and_the_dropped_in_sqlite_store_are_one_store() { // What the packer signs (the library's own door, read off the built cdylib) is what the @@ -443,8 +474,11 @@ fn the_linked_and_the_dropped_in_sqlite_store_are_one_store() { assert_eq!(r(2), "append_batch replay = Ok(Head { seq: 2, epoch: 0 })"); assert!(r(3).starts_with("reserve 4 = Ok(["), "{replays:?}"); assert!(r(4).contains("Exhausted"), "{replays:?}"); +} - // RED (a): the store's door asked for as a secret is refused, through both doors. +/// RED (a): the store's door asked for as a secret is refused, through both doors. +#[test] +fn a_store_door_loaded_as_another_kind_is_refused() { let d = Dispatcher::new(DispatchConfig::default()); for by in [Door::Linked, Door::Dropped] { match load::(by, &d) { @@ -458,9 +492,13 @@ fn the_linked_and_the_dropped_in_sqlite_store_are_one_store() { ), } } +} - // RED (b): the dropped-in door on a file that already holds a foreign key is NOT the same - // transcript — the equality above is not vacuous. +/// RED (b): the dropped-in door on a file that already holds a foreign key is NOT the same +/// transcript as the linked one — the equality in the both-ways test is not vacuous. +#[test] +fn a_store_holding_a_foreign_row_does_not_compare_equal() { + let linked = transcript(Door::Linked, "linked-red", None); let red = transcript(Door::Dropped, "red", Some("vk_foreign")); assert_ne!( red, linked, diff --git a/store-sqlite-plugin/tests/e2e.rs b/store-sqlite-plugin/tests/e2e.rs index c49a1dc..1251959 100644 --- a/store-sqlite-plugin/tests/e2e.rs +++ b/store-sqlite-plugin/tests/e2e.rs @@ -232,6 +232,12 @@ fn plugin_path() -> PathBuf { fresh } +/// The node's one `op_id` allocator, as the kernel hands a store handle its own: the bridge's +/// additive writes mint from it. Its node half is one no test op id uses. +fn mint() -> busbar_contract::abi::store::OpId { + static N: AtomicU64 = AtomicU64::new(0); + busbar_contract::abi::store::OpId::from_parts(0x5e1f, N.fetch_add(1, Ordering::Relaxed) + 1) +} /// THE DROPPED-IN DOOR, opened as the host opens a store: the cdylib at `path` `dlopen`ed by the /// loader's `load_dropped` against the Statement rendering its own door states (what /// `busbar-plugin-pack` signs into the manifest), bound to a real dispatcher, then `open`ed on @@ -250,7 +256,7 @@ fn door_store(path: &std::path::Path, cfg: &str) -> Result conns: None, }; let plugin = load_dropped::(path, &stated, bind).map_err(|e| e.to_string())?; - LoadedStore::open(plugin, d, cfg.as_bytes(), 1) + LoadedStore::open(plugin, d, cfg.as_bytes(), mint) } /// `close` the instance, as the host does at shutdown: its connections to the file close with it. diff --git a/store-sqlite/Cargo.toml b/store-sqlite/Cargo.toml index 747ff01..9368a3e 100644 --- a/store-sqlite/Cargo.toml +++ b/store-sqlite/Cargo.toml @@ -20,7 +20,7 @@ crate-type = ["rlib"] # as `busbar_contract::abi::sdk`). Pinned by git rev to the busbar commit in `.busbar-ref`; a busbar # build that links this crate `[patch]`es the busbar git source to its own tree, so ONE copy of the # contract links. -busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "e1d3d8b097201859751d77208e66249d98b50a4b" } +busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "bf32f11ba9634c77afbe1dc89a8c3986499b38af" } # `bundled` compiles SQLite from source so the plugin has no system libsqlite3 dependency at all. rusqlite = { version = "0.40.0", features = ["bundled"] } serde_json = "1" @@ -28,6 +28,10 @@ serde_json = "1" # conformance suite's stand-in bodies). serde = { version = "1", features = ["derive"] } -# NO DEV-DEPENDENCIES. The `RecordStore` contract conformance suite lives in this crate's own -# `src/tests/store_conformance.rs` (#2/#31 forbid a shared test util between plugins); all it needs is -# `busbar-contract`, already a normal dependency above. +# The `RecordStore` contract conformance suite lives in this crate's own +# `src/tests/store_conformance.rs` (#2/#31 forbid a shared test util between plugins). The one +# dev-only edge is the contract's own `test-seal` feature: it compiles `busbar_contract::testkit`, +# whose `store_v3` cases are the store kind's epoch and slice-life spec every durable store runs +# (`abi::store::SLICE_TTL_MS`). Same pinned rev, so one contract links. +[dev-dependencies] +busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "bf32f11ba9634c77afbe1dc89a8c3986499b38af", features = ["test-seal"] } diff --git a/store-sqlite/declares.json b/store-sqlite/declares.json index 344abf0..985cfd5 100644 --- a/store-sqlite/declares.json +++ b/store-sqlite/declares.json @@ -1,6 +1,6 @@ { "contract_abi": { - "min": 3, - "max": 3 + "min": 4, + "max": 4 } } diff --git a/store-sqlite/src/door.rs b/store-sqlite/src/door.rs index 240f969..6963b2e 100644 --- a/store-sqlite/src/door.rs +++ b/store-sqlite/src/door.rs @@ -30,50 +30,78 @@ busbar_contract::store_door!(SqliteStore, NAME, env!("CARGO_PKG_VERSION"), 64); /// be swallowed into quietly opening the default relative path — that reads as a healthy boot /// against an empty governance database (data loss), not a config error. pub fn open(cfg: &str) -> Result { - let v: serde_json::Value = if cfg.trim().is_empty() { - serde_json::Value::Object(Default::default()) - } else { - serde_json::from_str(cfg).map_err(|e| format!("invalid sqlite plugin config: {e}"))? - }; - let path = match v.get("db_path") { - None | Some(serde_json::Value::Null) => "busbar-governance.db", - Some(serde_json::Value::String(s)) => s.as_str(), - Some(other) => { - return Err(format!( - "invalid sqlite plugin config: `db_path` must be a string, got {other}" - )) - } - }; - let busy_timeout_ms = match v.get("busy_timeout_ms") { - None | Some(serde_json::Value::Null) => 5000, - Some(serde_json::Value::Number(n)) if n.is_i64() || n.is_u64() => { - let ms = n.as_i64().ok_or_else(|| { - format!("invalid sqlite plugin config: `busy_timeout_ms` out of range, got {n}") - })?; - // A negative duration has no meaning and can only be a config mistake (a unit- - // conversion bug, a stray sign, a bad template substitution) -- unlike `0`, which is a - // real, deliberate SQLite setting (see `apply_pragmas`'s own doc: SQLite's own - // zero-second default), a negative number names nothing SQLite or an operator could - // sensibly mean. SQLite doesn't reject it either -- like `0`, any `busy_timeout <= 0` - // silently disables the busy handler entirely (every write fails instantly on the - // slightest lock contention instead of retrying), which reads as a healthy boot with a - // quietly degraded reliability posture. `0` is left as a legal, if unusual, explicit - // "never retry" choice; only the never-sensible negative case is rejected here, the - // same silent-footgun class the wrong-JSON-type check above already guards against. - if ms < 0 { + let Settings { + db_path, + busy_timeout_ms, + } = Settings::parse(cfg)?; + SqliteStore::open(&db_path, busy_timeout_ms).map_err(|e| e.0) +} + +/// The operator's settings, PARSED and nothing more: the store's `validate` slot runs this alone +/// (`--validate` opens, connects to and migrates no store), and [`open`] opens what it read. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Settings { + /// The database file (or an in-memory spelling). + pub db_path: String, + /// SQLite's busy timeout, in ms; never negative. + pub busy_timeout_ms: i64, +} + +impl Settings { + /// Parse the section's JSON exactly as [`open`] reads it. + /// + /// # Errors + /// The refusal text [`open`] carries for the same settings. + pub fn parse(cfg: &str) -> Result { + const BAD: &str = "invalid sqlite plugin config"; + let v: serde_json::Value = if cfg.trim().is_empty() { + serde_json::Value::Object(Default::default()) + } else { + serde_json::from_str(cfg).map_err(|e| format!("invalid sqlite plugin config: {e}"))? + }; + let path = match v.get("db_path") { + None | Some(serde_json::Value::Null) => "busbar-governance.db".to_owned(), + Some(serde_json::Value::String(s)) => s.clone(), + Some(other) => { + return Err(format!( + "invalid sqlite plugin config: `db_path` must be a string, got {other}" + )) + } + }; + let busy_timeout_ms = match v.get("busy_timeout_ms") { + None | Some(serde_json::Value::Null) => 5000, + Some(serde_json::Value::Number(n)) if n.is_i64() || n.is_u64() => { + let ms = n.as_i64().ok_or_else(|| { + format!("invalid sqlite plugin config: `busy_timeout_ms` out of range, got {n}") + })?; + // A negative duration has no meaning and can only be a config mistake (a unit- + // conversion bug, a stray sign, a bad template substitution) -- unlike `0`, which is a + // real, deliberate SQLite setting (see `apply_pragmas`'s own doc: SQLite's own + // zero-second default), a negative number names nothing SQLite or an operator could + // sensibly mean. SQLite doesn't reject it either -- like `0`, any `busy_timeout <= 0` + // silently disables the busy handler entirely (every write fails instantly on the + // slightest lock contention instead of retrying), which reads as a healthy boot with a + // quietly degraded reliability posture. `0` is left as a legal, if unusual, explicit + // "never retry" choice; only the never-sensible negative case is rejected here, the + // same silent-footgun class the wrong-JSON-type check above already guards against. + if ms < 0 { + return Err(format!( + "{BAD}: `busy_timeout_ms` must not be negative, got {ms}" + )); + } + ms + } + Some(other) => { return Err(format!( - "invalid sqlite plugin config: `busy_timeout_ms` must not be negative, got {ms}" - )); + "{BAD}: `busy_timeout_ms` must be an integer, got {other}" + )) } - ms - } - Some(other) => { - return Err(format!( - "invalid sqlite plugin config: `busy_timeout_ms` must be an integer, got {other}" - )) - } - }; - SqliteStore::open(path, busy_timeout_ms).map_err(|e| e.0) + }; + Ok(Self { + db_path: path, + busy_timeout_ms, + }) + } } // ── unit tests for the door's own responsibility: adapting the engine's JSON config into a real diff --git a/store-sqlite/src/lib.rs b/store-sqlite/src/lib.rs index 0ddf118..eaddd89 100644 --- a/store-sqlite/src/lib.rs +++ b/store-sqlite/src/lib.rs @@ -22,8 +22,8 @@ use busbar_contract::records::{ UNIT_CACHE_WRITE, UNIT_INPUT, UNIT_OUTPUT, }; use rusqlite::{params, Connection, OptionalExtension, TransactionBehavior}; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Mutex; +use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; // rusqlite error -> the api's backend-agnostic `RecordStoreError` (the contract crate stays storage-free, // so the `From` impl that powers `?` cannot live there). Replace `?` with `.store()?`. @@ -100,7 +100,12 @@ impl IntoStoreResult for Result { /// caps, drawn totals and slices (`money_caps`, `money_used`, `money_slices`), the ledger streams /// (`journal`), the session directory (`sessions`) and a plane's kernel-held records /// (`schema_records`). Additive on the same terms as v7-v9: new tables only, created by `SCHEMA`. -const SCHEMA_VERSION: i64 = 11; +/// +/// v12 (busbar 1.6.0, the store kind's epoch and slice life, `abi::store::SLICE_TTL_MS` (a)-(c)): +/// the ONE persisted fleet epoch (`money_epoch`) and each slice's `valid_until_ms`. Additive: a new +/// table, and a nullable-free `ADD COLUMN` whose default (`-1`, `u64::MAX` bit-for-bit) is exactly the +/// never-expiring validity every v11 slice was granted, so no open slice changes meaning. +const SCHEMA_VERSION: i64 = 12; /// The task states that are TERMINAL — used ONLY by the v10 migration, to set the `disposition` /// sidecar on a task row copied out of the legacy typed `tasks` table (a live 1.6.0 engine sets it @@ -335,10 +340,20 @@ CREATE TABLE IF NOT EXISTS money_used ( ) STRICT, WITHOUT ROWID; -- AUTOINCREMENT: a slice id is never reused, so a late release of a closed slice can never land on -- a newer one. +-- A slice stays a row once it is closed (`remaining` 0): any further release of it answers 0 and is +-- not refused as never granted. `valid_until_ms` is a u64 kept bit-for-bit (-1 = never expires, a +-- v11 slice). CREATE TABLE IF NOT EXISTS money_slices ( - slice_id INTEGER PRIMARY KEY AUTOINCREMENT, - slot TEXT NOT NULL, - remaining INTEGER NOT NULL + slice_id INTEGER PRIMARY KEY AUTOINCREMENT, + slot TEXT NOT NULL, + remaining INTEGER NOT NULL, + valid_until_ms INTEGER NOT NULL DEFAULT -1 +) STRICT; +-- The ONE fleet epoch (v12, `abi::store::SLICE_TTL_MS` (a)): 0 before any reserve; only a reserve +-- raises it, nothing lowers it. +CREATE TABLE IF NOT EXISTS money_epoch ( + id INTEGER NOT NULL PRIMARY KEY CHECK (id = 0), + epoch INTEGER NOT NULL ) STRICT; -- The ledger streams: `seq` counts from 1 per stream, so a stream's head is its MAX(seq). @@ -492,6 +507,10 @@ pub struct SqliteStore { readers: Vec>, next_reader: AtomicUsize, path: String, + /// The clock this store reads, in ms since the epoch: `None` (always, outside its own tests) is + /// the wall clock; its tests hand it one they hold still and move to expire a slice + /// (`busbar_contract::testkit::store_v3::Harness`). + test_clock_ms: Option>, } impl SqliteStore { @@ -538,6 +557,7 @@ impl SqliteStore { readers, next_reader: AtomicUsize::new(0), path: path.to_string(), + test_clock_ms: None, }; store.migrate()?; Ok(store) @@ -554,11 +574,30 @@ impl SqliteStore { readers: Vec::new(), next_reader: AtomicUsize::new(0), path: ":memory:".to_string(), + test_clock_ms: None, }; store.migrate()?; Ok(store) } + /// This store's clock, in ms since the epoch. + fn now_ms(&self) -> u64 { + if let Some(clock) = &self.test_clock_ms { + return clock.load(Ordering::Relaxed); + } + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX)) + .unwrap_or(0) + } + + /// Run this store on `clock` (its tests' clock, in ms). + #[cfg(test)] + fn on_clock(mut self, clock: Arc) -> Self { + self.test_clock_ms = Some(clock); + self + } + fn lock_writer(&self) -> std::sync::MutexGuard<'_, Connection> { self.writer.lock().unwrap_or_else(|p| p.into_inner()) } @@ -660,6 +699,18 @@ impl SqliteStore { { rebuild_usage_metering_for_v10(&tx)?; } + // v12: a v11 `money_slices` gains `valid_until_ms` (its default is the never-expiring + // validity v11 granted). Keyed on the column's absence, so a re-run is a no-op, and run + // BEFORE `SCHEMA` for the same reason as the v10 rebuild above. + if table_exists(&tx, "money_slices")? + && !column_exists(&tx, "money_slices", "valid_until_ms")? + { + tx.execute( + "ALTER TABLE money_slices ADD COLUMN valid_until_ms INTEGER NOT NULL DEFAULT -1", + [], + ) + .store()?; + } tx.execute_batch(SCHEMA).store()?; tx.execute( "INSERT INTO store_revision (id, revision) VALUES (0, 0) ON CONFLICT(id) DO NOTHING", diff --git a/store-sqlite/src/tests/v3_tests.rs b/store-sqlite/src/tests/v3_tests.rs index b9b241c..637bdf9 100644 --- a/store-sqlite/src/tests/v3_tests.rs +++ b/store-sqlite/src/tests/v3_tests.rs @@ -66,20 +66,20 @@ fn reserve( cells: &[Cell<'_>], ) -> Result, ReserveRefused> { let mut grants = Vec::new(); - s.reserve(op, epoch, cells.iter().copied(), &mut grants) + s.v3_reserve(op, epoch, cells.iter().copied(), &mut grants) .map(|()| grants) } /// `slice_release`, its amounts collected. fn release(s: &SqliteStore, op: OpId, epoch: u64, items: &[(u64, u64)]) -> OpResult> { let mut released = Vec::new(); - s.slice_release(op, epoch, items.iter().copied(), &mut released) + s.v3_slice_release(op, epoch, items.iter().copied(), &mut released) .map(|()| released) } fn capped(dimension: Dimension<'static>, c: u64) -> SqliteStore { let s = fresh(); - s.window_caps(op(1_000_000), &[cap(dimension, c, 1)]) + s.v3_window_caps(op(1_000_000), &[cap(dimension, c, 1)]) .expect("caps"); s } @@ -124,42 +124,52 @@ fn the_sqlite_store_states_it_is_durable_and_refuses_forks() { fn a_replayed_usage_batch_applies_once() { let s = fresh(); let cells = [("k", 60, delta(1, 10))]; - s.add_usage_batch(op(1), &cells).expect("first"); - s.add_usage_batch(op(1), &cells) + s.v3_add_usage_batch(op(1), &cells).expect("first"); + s.v3_add_usage_batch(op(1), &cells) .expect("replay answers the original"); - assert_eq!(s.get_usage("k", 60).expect("read").requests, 1); + assert_eq!( + RecordStore::get_usage(&s, "k", 60).expect("read").requests, + 1 + ); } #[test] fn equal_bodies_under_distinct_op_ids_both_apply() { let s = fresh(); let cells = [("k", 60, delta(2, 4))]; - s.add_usage_batch(op(1), &cells).expect("a"); - s.add_usage_batch(op(2), &cells).expect("b"); - assert_eq!(s.get_usage("k", 60).expect("read").requests, 4); + s.v3_add_usage_batch(op(1), &cells).expect("a"); + s.v3_add_usage_batch(op(2), &cells).expect("b"); + assert_eq!( + RecordStore::get_usage(&s, "k", 60).expect("read").requests, + 4 + ); } #[test] fn a_reused_op_id_with_a_different_body_is_a_conflict_and_applies_nothing() { let s = fresh(); - s.add_usage_batch(op(1), &[("k", 60, delta(1, 1))]) + s.v3_add_usage_batch(op(1), &[("k", 60, delta(1, 1))]) .expect("first"); assert_eq!( - s.add_usage_batch(op(1), &[("k", 60, delta(5, 5))]), + s.v3_add_usage_batch(op(1), &[("k", 60, delta(5, 5))]), Err(OpRefused::Conflict) ); - assert_eq!(s.get_usage("k", 60).expect("read").requests, 1); + assert_eq!( + RecordStore::get_usage(&s, "k", 60).expect("read").requests, + 1 + ); } #[test] fn an_op_id_reused_across_slots_is_a_conflict() { let s = fresh(); - s.add_usage_op(op(1), "k", 60, &delta(1, 1)).expect("usage"); + s.v3_add_usage_op(op(1), "k", 60, &delta(1, 1)) + .expect("usage"); assert_eq!( - s.append_audit_op(op(1), &audit(1, "a")), + s.v3_append_audit_op(op(1), &audit(1, "a")), Err(OpRefused::Conflict) ); - assert!(s.list_audit().expect("list").is_empty()); + assert!(RecordStore::list_audit(&s).expect("list").is_empty()); } #[test] @@ -168,12 +178,12 @@ fn a_failed_write_is_not_recorded_so_a_retry_is_evaluated_afresh() { s.append_audit(&audit(1, "a")).expect("seed"); // A fork FAILS and is not recorded under the op_id ... assert!(matches!( - s.append_audit_op(op(9), &audit(1, "forked")), + s.v3_append_audit_op(op(9), &audit(1, "forked")), Err(OpRefused::Failed(_)) )); // ... so the same op_id with a different, applicable body is new, not a conflict. - s.append_audit_op(op(9), &audit(2, "b")).expect("fresh"); - assert_eq!(s.list_audit().expect("list").len(), 2); + s.v3_append_audit_op(op(9), &audit(2, "b")).expect("fresh"); + assert_eq!(RecordStore::list_audit(&s).expect("list").len(), 2); } #[test] @@ -182,14 +192,14 @@ fn an_audit_batch_with_one_fork_applies_none_of_it() { s.append_audit(&audit(2, "a")).expect("seed"); let batch = [audit(1, "x"), audit(2, "forked")]; assert!(matches!( - s.append_audit_batch(op(1), &batch), + s.v3_append_audit_batch(op(1), &batch), Err(OpRefused::Failed(_)) )); - assert_eq!(s.list_audit().expect("list").len(), 1); + assert_eq!(RecordStore::list_audit(&s).expect("list").len(), 1); // Two different records at one seq INSIDE the batch are a fork too. let inner = [audit(5, "x"), audit(5, "y")]; - assert!(s.append_audit_batch(op(2), &inner).is_err()); - assert_eq!(s.list_audit().expect("list").len(), 1); + assert!(s.v3_append_audit_batch(op(2), &inner).is_err()); + assert_eq!(RecordStore::list_audit(&s).expect("list").len(), 1); } #[test] @@ -205,10 +215,15 @@ fn a_usage_batch_applies_its_cells_in_order() { billable_requests: 1, models: vec![], }; - s.add_usage_batch(op(1), &[("k", 60, neg), ("k", 60, pos)]) + s.v3_add_usage_batch(op(1), &[("k", 60, neg), ("k", 60, pos)]) .expect("batch"); // The floor at zero makes order matter: -1 then +1 is 1, not 0. - assert_eq!(s.get_usage("k", 60).expect("read").billable_requests, 1); + assert_eq!( + RecordStore::get_usage(&s, "k", 60) + .expect("read") + .billable_requests, + 1 + ); } #[test] @@ -230,11 +245,11 @@ fn a_metering_batch_replay_applies_once() { priced_from_ms: 0, usage_units: Default::default(), }; - s.add_metering_batch(op(1), std::slice::from_ref(&d)) + s.v3_add_metering_batch(op(1), std::slice::from_ref(&d)) .expect("a"); - s.add_metering_batch(op(1), std::slice::from_ref(&d)) + s.v3_add_metering_batch(op(1), std::slice::from_ref(&d)) .expect("replay"); - let rows = s.list_metering(86_400).expect("list"); + let rows = RecordStore::list_metering(&s, 86_400).expect("list"); assert_eq!(rows.iter().map(|r| r.requests).sum::(), 2); } @@ -300,7 +315,7 @@ fn an_overflowing_draw_is_exhausted_not_wrapped() { #[test] fn a_chain_draw_is_all_or_nothing() { let s = fresh(); - s.window_caps( + s.v3_window_caps( op(100), &[ cap(Dimension::Requests, 10, 1), @@ -368,18 +383,120 @@ fn a_refused_reserve_is_not_recorded() { reserve(&s, op(2), 0, &[cell(Dimension::Requests, 1)]), Err(ReserveRefused::Exhausted { cell: 0 }) ); - s.window_caps(op(3), &[cap(Dimension::Requests, 2, 2)]) + s.v3_window_caps(op(3), &[cap(Dimension::Requests, 2, 2)]) .expect("raise"); // The same op_id is evaluated afresh and now fits. reserve(&s, op(2), 0, &[cell(Dimension::Requests, 1)]).expect("afresh"); } +/// The store kind's shared epoch and slice-life cases (`abi::store::SLICE_TTL_MS` (a)-(c)), run over +/// ONE file this harness reopens, on a clock it moves. The store is durable and shared by every node +/// that opens the file, so it runs the FLEET cases (`busbar_contract::testkit::store_v3`). +struct FileHarness { + path: String, + clock: std::sync::Arc, +} + +/// A test clock, held still at a real instant. +fn still_clock() -> std::sync::Arc { + std::sync::Arc::new(std::sync::atomic::AtomicU64::new(1_790_000_000_000)) +} + +impl busbar_contract::testkit::store_v3::Harness for FileHarness { + type Store = SqliteStore; + fn open(&self) -> SqliteStore { + SqliteStore::open(&self.path, 5000) + .expect("open") + .on_clock(std::sync::Arc::clone(&self.clock)) + } + fn now_ms(&self) -> u64 { + self.clock.load(std::sync::atomic::Ordering::Relaxed) + } + fn advance_ms(&self, ms: u64) { + self.clock + .fetch_add(ms, std::sync::atomic::Ordering::Relaxed); + } +} + #[test] -fn the_sqlite_store_never_answers_a_stale_epoch() { +fn the_sqlite_store_follows_the_fleet_epoch_and_slice_life() { + let (s, path) = on_file("fleet"); + drop(s); + busbar_contract::testkit::store_v3::fleet_store(&FileHarness { + path, + clock: still_clock(), + }); +} + +#[test] +fn a_stale_epoch_is_refused_and_records_nothing_under_its_op_id() { let s = capped(Dimension::Requests, 10); reserve(&s, op(1), 9, &[cell(Dimension::Requests, 1)]).expect("epoch 9"); - reserve(&s, op(2), 1, &[cell(Dimension::Requests, 1)]) - .expect("an older epoch is not stale on a node-local store"); + assert_eq!( + reserve(&s, op(2), 1, &[cell(Dimension::Requests, 1)]), + Err(ReserveRefused::StaleEpoch) + ); + // Nothing was recorded under op 2: the same id at the current epoch applies afresh. + reserve(&s, op(2), 9, &[cell(Dimension::Requests, 1)]).expect("afresh at epoch 9"); +} + +#[test] +fn a_grant_is_valid_for_slice_ttl_on_the_stores_clock() { + let skew = still_clock(); + let s = capped(Dimension::Requests, 10).on_clock(std::sync::Arc::clone(&skew)); + let g = reserve(&s, op(1), 0, &[cell(Dimension::Requests, 3)]).expect("draw"); + assert_eq!(g[0].valid_until_ms, s.now_ms() + SLICE_TTL_MS); + // Past its validity a release returns nothing: expiry already gave the 3 back. + skew.fetch_add(SLICE_TTL_MS + 1, std::sync::atomic::Ordering::Relaxed); + assert_eq!(release(&s, op(2), 0, &[(g[0].slice_id, 3)]), Ok(vec![0])); + reserve(&s, op(3), 0, &[cell(Dimension::Requests, 10)]).expect("all 10 drawable again"); +} + +/// v11 -> v12: a slice granted before the crossing keeps the never-expiring validity it was granted, +/// and the file gains the epoch row's table. +#[test] +fn a_v11_slice_crosses_to_v12_never_expiring() { + let (s, path) = on_file("v11"); + s.v3_window_caps(op(1), &[cap(Dimension::Requests, 10, 1)]) + .expect("caps"); + drop(s); + { + let conn = rusqlite::Connection::open(&path).expect("raw open"); + conn.execute_batch( + "DROP TABLE money_epoch; + DROP TABLE money_slices; + CREATE TABLE money_slices ( + slice_id INTEGER PRIMARY KEY AUTOINCREMENT, + slot TEXT NOT NULL, + remaining INTEGER NOT NULL + ) STRICT; + PRAGMA user_version = 11;", + ) + .expect("a v11 file"); + let (slot, _) = slot_of(&key(Dimension::Requests)); + conn.execute( + "INSERT INTO money_slices (slot, remaining) VALUES (?1, 4)", + [&slot], + ) + .expect("a v11 slice"); + conn.execute( + "INSERT INTO money_used (slot, used) VALUES (?1, 4)", + [&slot], + ) + .expect("its draw"); + } + let skew = still_clock(); + let s = SqliteStore::open(&path, 5000) + .expect("reopen at v12") + .on_clock(std::sync::Arc::clone(&skew)); + skew.fetch_add(10 * SLICE_TTL_MS, std::sync::atomic::Ordering::Relaxed); + // Still drawn: 6 fit, 7 do not. + assert_eq!( + reserve(&s, op(2), 0, &[cell(Dimension::Requests, 7)]), + Err(ReserveRefused::Exhausted { cell: 0 }) + ); + assert_eq!(release(&s, op(3), 0, &[(1, 4)]), Ok(vec![4])); + reserve(&s, op(4), 0, &[cell(Dimension::Requests, 10)]).expect("the release freed all 4"); } #[test] @@ -417,16 +534,16 @@ fn releasing_an_unknown_slice_fails_and_applies_nothing() { #[test] fn window_caps_newest_generation_wins_and_equal_generation_conflicts() { let s = fresh(); - s.window_caps(op(1), &[cap(Dimension::Requests, 1, 5)]) + s.v3_window_caps(op(1), &[cap(Dimension::Requests, 1, 5)]) .expect("gen 5"); - s.window_caps(op(2), &[cap(Dimension::Requests, 99, 4)]) + s.v3_window_caps(op(2), &[cap(Dimension::Requests, 99, 4)]) .expect("an older generation is ignored"); assert_eq!( reserve(&s, op(3), 0, &[cell(Dimension::Requests, 2)]), Err(ReserveRefused::Exhausted { cell: 0 }) ); assert_eq!( - s.window_caps( + s.v3_window_caps( op(4), &[ cap(Dimension::Requests, 3, 6), @@ -441,7 +558,7 @@ fn window_caps_newest_generation_wins_and_equal_generation_conflicts() { reserve(&s, op(5), 0, &[cell(Dimension::Requests, 2)]), Err(ReserveRefused::Exhausted { cell: 0 }) ); - s.window_caps(op(6), &[cap(Dimension::Requests, 3, 6)]) + s.v3_window_caps(op(6), &[cap(Dimension::Requests, 3, 6)]) .expect("gen 6"); reserve(&s, op(7), 0, &[cell(Dimension::Requests, 2)]).expect("cap 3"); } @@ -449,7 +566,7 @@ fn window_caps_newest_generation_wins_and_equal_generation_conflicts() { #[test] fn an_op_id_is_forgotten_after_its_retention() { let s = fresh(); - s.add_usage_batch(op(1), &[("k", 60, delta(1, 1))]) + s.v3_add_usage_batch(op(1), &[("k", 60, delta(1, 1))]) .expect("a"); // Age op 1 past its retention, as the clock would. let old = now_secs() - OP_ID_RETENTION_SECS as i64; @@ -460,11 +577,14 @@ fn an_op_id_is_forgotten_after_its_retention() { ) .expect("age"); // Recording another op sweeps the expired one; the old op_id then reads as new. - s.add_usage_batch(op(2), &[("j", 60, delta(1, 1))]) + s.v3_add_usage_batch(op(2), &[("j", 60, delta(1, 1))]) .expect("b"); - s.add_usage_batch(op(1), &[("k", 60, delta(1, 1))]) + s.v3_add_usage_batch(op(1), &[("k", 60, delta(1, 1))]) .expect("new again"); - assert_eq!(s.get_usage("k", 60).expect("read").requests, 2); + assert_eq!( + RecordStore::get_usage(&s, "k", 60).expect("read").requests, + 2 + ); } #[test] @@ -472,19 +592,22 @@ fn append_batch_advances_the_stream_head_once_per_op() { let s = fresh(); let r = |b: u8| RecordBytes::new(vec![b]).expect("record"); assert_eq!( - s.append_batch(op(1), "journal", &[r(1), r(2)]) + s.v3_append_batch(op(1), "journal", &[r(1), r(2)]) .expect("a") .seq, 2 ); assert_eq!( - s.append_batch(op(1), "journal", &[r(1), r(2)]) + s.v3_append_batch(op(1), "journal", &[r(1), r(2)]) .expect("replay") .seq, 2 ); - assert_eq!(s.append_batch(op(2), "journal", &[r(3)]).expect("b").seq, 3); - let heads = s.heads().expect("heads"); + assert_eq!( + s.v3_append_batch(op(2), "journal", &[r(3)]).expect("b").seq, + 3 + ); + let heads = s.v3_heads().expect("heads"); assert_eq!(heads.len(), 1); assert_eq!(heads[0].0, "journal"); assert_eq!(heads[0].1.seq, 3); @@ -493,17 +616,17 @@ fn append_batch_advances_the_stream_head_once_per_op() { #[test] fn sessions_are_listed_per_principal_and_removed() { let s = fresh(); - s.session_put(1, "n1", "alice").expect("put"); - s.session_put(2, "n2", "alice").expect("put"); - s.session_put(3, "n1", "bob").expect("put"); + s.v3_session_put(1, "n1", "alice").expect("put"); + s.v3_session_put(2, "n2", "alice").expect("put"); + s.v3_session_put(3, "n1", "bob").expect("put"); assert_eq!( - s.sessions_for("alice").expect("list"), + s.v3_sessions_for("alice").expect("list"), vec![(1, "n1".to_string()), (2, "n2".to_string())] ); - s.session_remove(1).expect("remove"); - s.session_remove(1).expect("absent is Ok"); + s.v3_session_remove(1).expect("remove"); + s.v3_session_remove(1).expect("absent is Ok"); assert_eq!( - s.sessions_for("alice").expect("list"), + s.v3_sessions_for("alice").expect("list"), vec![(2, "n2".to_string())] ); } @@ -511,22 +634,23 @@ fn sessions_are_listed_per_principal_and_removed() { #[test] fn schema_records_upsert_read_back_and_scan_a_prefix_in_key_order() { let s = fresh(); - s.record_put("a", b"k\x01", b"one").expect("put"); - s.record_put("a", b"k\x00", b"zero").expect("put"); - s.record_put("a", b"k\xff", b"max").expect("put"); - s.record_put("a", b"l", b"next").expect("put"); - s.record_put("b", b"k\x00", b"other schema").expect("put"); - s.record_put("a", b"k\x01", b"one again") + s.v3_record_put("a", b"k\x01", b"one").expect("put"); + s.v3_record_put("a", b"k\x00", b"zero").expect("put"); + s.v3_record_put("a", b"k\xff", b"max").expect("put"); + s.v3_record_put("a", b"l", b"next").expect("put"); + s.v3_record_put("b", b"k\x00", b"other schema") + .expect("put"); + s.v3_record_put("a", b"k\x01", b"one again") .expect("overwrite"); let read = |k: &[u8]| { - s.record_get("a", k) + s.v3_record_get("a", k) .expect("get") .map(|r| r.as_slice().to_vec()) }; assert_eq!(read(b"k\x01"), Some(b"one again".to_vec())); assert_eq!(read(b"missing"), None); let keys = |prefix: &[u8], limit: u32| -> Vec> { - s.record_scan("a", prefix, limit) + s.v3_record_scan("a", prefix, limit) .expect("scan") .into_iter() .map(|(k, _)| k) @@ -559,16 +683,16 @@ fn prefix_successor_bounds_every_key_the_prefix_starts() { #[test] fn dedupe_and_money_state_survive_a_reopen_of_the_file() { let (s, path) = on_file("durable"); - s.window_caps(op(1), &[cap(Dimension::Requests, 10, 1)]) + s.v3_window_caps(op(1), &[cap(Dimension::Requests, 10, 1)]) .expect("caps"); let grants = reserve(&s, op(2), 0, &[cell(Dimension::Requests, 6)]).expect("draw"); - s.add_usage_batch(op(3), &[("k", 60, delta(1, 1))]) + s.v3_add_usage_batch(op(3), &[("k", 60, delta(1, 1))]) .expect("usage"); let r = |b: u8| RecordBytes::new(vec![b]).expect("record"); - s.append_batch(op(4), "journal", &[r(1), r(2)]) + s.v3_append_batch(op(4), "journal", &[r(1), r(2)]) .expect("journal"); - s.session_put(9, "n1", "alice").expect("session"); - s.record_put("plane", b"k", b"v").expect("record"); + s.v3_session_put(9, "n1", "alice").expect("session"); + s.v3_record_put("plane", b"k", b"v").expect("record"); drop(s); let s = SqliteStore::open(&path, 5000).expect("reopen"); @@ -586,22 +710,25 @@ fn dedupe_and_money_state_survive_a_reopen_of_the_file() { reserve(&s, op(5), 0, &[cell(Dimension::Requests, 5)]), Err(ReserveRefused::Exhausted { cell: 0 }) ); - s.add_usage_batch(op(3), &[("k", 60, delta(1, 1))]) + s.v3_add_usage_batch(op(3), &[("k", 60, delta(1, 1))]) .expect("replay"); - assert_eq!(s.get_usage("k", 60).expect("read").requests, 1); assert_eq!( - s.append_batch(op(4), "journal", &[r(1), r(2)]) + RecordStore::get_usage(&s, "k", 60).expect("read").requests, + 1 + ); + assert_eq!( + s.v3_append_batch(op(4), "journal", &[r(1), r(2)]) .expect("replay") .seq, 2 ); - assert_eq!(s.heads().expect("heads")[0].1.seq, 2); + assert_eq!(s.v3_heads().expect("heads")[0].1.seq, 2); assert_eq!( - s.sessions_for("alice").expect("sessions"), + s.v3_sessions_for("alice").expect("sessions"), vec![(9, "n1".to_string())] ); assert_eq!( - s.record_get("plane", b"k") + s.v3_record_get("plane", b"k") .expect("get") .map(|v| v.as_slice().to_vec()), Some(b"v".to_vec()) @@ -626,19 +753,25 @@ fn a_replayed_plane_record_append_applies_once_and_a_fork_fails_unrecorded() { disposition: busbar_contract::records::PlaneDisposition::Active, body, }; - s.append_plane_record_op(op(1), rec(b"{}")).expect("append"); - s.append_plane_record_op(op(1), rec(b"{}")).expect("replay"); + s.v3_append_plane_record_op(op(1), rec(b"{}")) + .expect("append"); + s.v3_append_plane_record_op(op(1), rec(b"{}")) + .expect("replay"); assert_eq!( - s.append_plane_record_op(op(1), rec(b"{\"x\":1}")), + s.v3_append_plane_record_op(op(1), rec(b"{\"x\":1}")), Err(OpRefused::Conflict) ); assert!(matches!( - s.append_plane_record_op(op(2), rec(b"{\"x\":1}")), + s.v3_append_plane_record_op(op(2), rec(b"{\"x\":1}")), Err(OpRefused::Failed(_)) )); assert_eq!( - s.list_plane_records("task_event", &busbar_contract::records::PlaneSelector::All) - .expect("list"), + RecordStore::list_plane_records( + &s, + "task_event", + &busbar_contract::records::PlaneSelector::All + ) + .expect("list"), vec![b"{}".to_vec()] ); } diff --git a/store-sqlite/src/v3.rs b/store-sqlite/src/v3.rs index 94d46cb..5156741 100644 --- a/store-sqlite/src/v3.rs +++ b/store-sqlite/src/v3.rs @@ -14,23 +14,32 @@ //! value fields are a conflict; a refusal or a failure rolls back and records nothing. Records //! older than [`OP_ID_RETENTION_SECS`] are swept when a new one is recorded. //! -//! EPOCH: this store holds one constant epoch and never answers a stale one (`ReserveIn`'s doc, "A -//! node-local store"); every node that shares the file draws against the same rows under the one -//! write lock, so the `epoch` a caller states is accepted as given. +//! EPOCH AND SLICE LIFE (`abi::store::SLICE_TTL_MS` (a)-(c)): this store is durable (no +//! `MARK_EPHEMERAL`) and every node that shares the file draws against the same rows, so it is a +//! store a FLEET shares. It persists ONE epoch (`money_epoch`), raised only by a reserve presenting +//! a higher one, atomically with its draw; a reserve below it is `StaleEpoch` and applies nothing. +//! Every slice is valid until `now + SLICE_TTL_MS` on this store's clock; past that it is EXPIRED +//! and its unreturned remainder goes back to its window's headroom (applied lazily, inside the next +//! reserve or release that touches it, under the write lock). `slice_release` always applies, +//! whatever the epoch, capped at what the slice has left; a closed slice stays a row, so any +//! further release of it answers 0. //! //! GRANT SIZE: a cell grants its whole `amount` or the reserve fails; the per-dimension test is //! 1.5.5's, cited on `abi::store::ReserveIn`. use std::collections::HashMap; +use busbar_contract::abi::sdk::conn::Host; use busbar_contract::abi::sdk::store::{ - Cap, CapsRefused, Cell, CellKey, Dimension, Grant, OpRefused, OpResult, ReserveRefused, - StoreSlots, Tail, + Cap, CapsRefused, Cell, CellKey, Dimension, Grant, Op, OpRefused, OpResult, ReserveRefused, + Scanned, Step, StoreSlots, Tail, }; -use busbar_contract::abi::store::{OpId, OP_ID_RETENTION_SECS}; +use busbar_contract::abi::store::{OpId, OP_ID_RETENTION_SECS, SLICE_TTL_MS}; use busbar_contract::kinds::{Head, RecordBytes}; use busbar_contract::records::{ - AuditRecord, MeteringDelta, PlaneRecordRef, RecordStoreError, UsageDelta, + AuditRecord, CredentialMeta, CredentialSecret, MeteringDelta, MeteringRow, PlaneRecordRef, + PlaneSelector, RecordStore, RecordStoreError, RecordStoreResult, UsageDelta, UsageLedger, + VirtualKey, }; use rusqlite::{params, OptionalExtension, Transaction, TransactionBehavior}; use serde::{Deserialize, Serialize}; @@ -115,6 +124,54 @@ fn stored_used(tx: &Transaction<'_>, slot: &str) -> Result, slot: &str, now_ms: u64) -> Result<(), RecordStoreError> { + let back: i64 = tx + .query_row( + "SELECT COALESCE(SUM(remaining), 0) FROM money_slices WHERE slot=?1 AND remaining > 0 \ + AND valid_until_ms >= 0 AND valid_until_ms < ?2", + params![slot, to_db(now_ms)], + |r| r.get(0), + ) + .store()?; + if back == 0 { + return Ok(()); + } + tx.execute( + "UPDATE money_slices SET remaining=0 WHERE slot=?1 AND remaining > 0 \ + AND valid_until_ms >= 0 AND valid_until_ms < ?2", + params![slot, to_db(now_ms)], + ) + .store()?; + let used = stored_used(tx, slot)?; + set_used(tx, slot, used.saturating_sub(from_db(back))) +} + +/// Forget closed slices whose validity ended more than `OP_ID_RETENTION_SECS` ago: past that a +/// release of one is a replay the dedupe log has forgotten too. +fn sweep_closed(tx: &Transaction<'_>, now_ms: u64) -> Result<(), RecordStoreError> { + let horizon = now_ms.saturating_sub(OP_ID_RETENTION_SECS.saturating_mul(1000)); + tx.execute( + "DELETE FROM money_slices WHERE remaining = 0 AND valid_until_ms >= 0 \ + AND valid_until_ms < ?1", + params![to_db(horizon)], + ) + .store() + .map(drop) +} + +/// The persisted fleet epoch (`SLICE_TTL_MS` (a)): 0 before any reserve. +fn stored_epoch(tx: &Transaction<'_>) -> Result { + tx.query_row("SELECT epoch FROM money_epoch WHERE id=0", [], |r| { + r.get::<_, i64>(0) + }) + .optional() + .store() + .map(|e| e.map_or(0, from_db)) +} + fn set_used(tx: &Transaction<'_>, slot: &str, used: u64) -> Result<(), RecordStoreError> { tx.execute( "INSERT INTO money_used (slot, used) VALUES (?1, ?2) \ @@ -221,20 +278,10 @@ impl SqliteStore { } } -impl StoreSlots for SqliteStore { - const TAIL: Tail = Tail { - ephemeral: false, - durable_plane: true, - fork_refusal: true, - }; - - fn open(settings: &[u8]) -> Result { - let cfg = std::str::from_utf8(settings) - .map_err(|e| format!("invalid sqlite plugin config: not UTF-8: {e}"))?; - crate::door::open(cfg) - } - - fn add_usage_op( +/// The store v3 slots' own bodies: each op's ONE body, which the table's slot answers Ready (the +/// store is local-disk-bound and never pends). +impl SqliteStore { + pub(crate) fn v3_add_usage_op( &self, op: OpId, bucket: &str, @@ -249,7 +296,7 @@ impl StoreSlots for SqliteStore { .map(drop) } - fn add_metering_op(&self, op: OpId, delta: &MeteringDelta) -> OpResult<()> { + pub(crate) fn v3_add_metering_op(&self, op: OpId, delta: &MeteringDelta) -> OpResult<()> { let body = format!("add_metering:{delta:?}"); self.op(op, &body, |tx| { add_metering_in(tx, delta).map_err(failed)?; @@ -258,7 +305,7 @@ impl StoreSlots for SqliteStore { .map(drop) } - fn append_audit_op(&self, op: OpId, entry: &AuditRecord) -> OpResult<()> { + pub(crate) fn v3_append_audit_op(&self, op: OpId, entry: &AuditRecord) -> OpResult<()> { let body = format!("append_audit:{entry:?}"); self.op(op, &body, |tx| { append_audit_in(tx, entry).map_err(failed)?; @@ -267,7 +314,11 @@ impl StoreSlots for SqliteStore { .map(drop) } - fn append_plane_record_op(&self, op: OpId, record: PlaneRecordRef<'_>) -> OpResult<()> { + pub(crate) fn v3_append_plane_record_op( + &self, + op: OpId, + record: PlaneRecordRef<'_>, + ) -> OpResult<()> { let body = format!("append_plane_record:{record:?}"); self.op(op, &body, |tx| { append_plane_record_in(tx, record).map_err(failed)?; @@ -276,7 +327,12 @@ impl StoreSlots for SqliteStore { .map(drop) } - fn append_batch(&self, op: OpId, stream: &str, records: &[RecordBytes]) -> OpResult { + pub(crate) fn v3_append_batch( + &self, + op: OpId, + stream: &str, + records: &[RecordBytes], + ) -> OpResult { let body = format!("append_batch:{stream:?}:{records:?}"); let answer = self.op(op, &body, |tx| { let mut seq: u64 = tx @@ -305,7 +361,7 @@ impl StoreSlots for SqliteStore { } } - fn heads(&self) -> Result, String> { + pub(crate) fn v3_heads(&self) -> Result, String> { let conn = self.lock_reader(); let mut stmt = conn .prepare("SELECT stream, MAX(seq) FROM journal GROUP BY stream ORDER BY stream") @@ -324,7 +380,12 @@ impl StoreSlots for SqliteStore { rows.collect::>().map_err(|e| e.to_string()) } - fn session_put(&self, session: u64, node: &str, principal: &str) -> Result<(), String> { + pub(crate) fn v3_session_put( + &self, + session: u64, + node: &str, + principal: &str, + ) -> Result<(), String> { self.lock_writer() .execute( "INSERT INTO sessions (session, node, principal) VALUES (?1, ?2, ?3) \ @@ -335,7 +396,7 @@ impl StoreSlots for SqliteStore { .map_err(|e| e.to_string()) } - fn session_remove(&self, session: u64) -> Result<(), String> { + pub(crate) fn v3_session_remove(&self, session: u64) -> Result<(), String> { self.lock_writer() .execute( "DELETE FROM sessions WHERE session=?1", @@ -345,7 +406,7 @@ impl StoreSlots for SqliteStore { .map_err(|e| e.to_string()) } - fn sessions_for(&self, principal: &str) -> Result, String> { + pub(crate) fn v3_sessions_for(&self, principal: &str) -> Result, String> { let conn = self.lock_reader(); let mut stmt = conn .prepare("SELECT session, node FROM sessions WHERE principal=?1") @@ -359,7 +420,12 @@ impl StoreSlots for SqliteStore { Ok(rows) } - fn record_put(&self, schema: &str, key: &[u8], value: &[u8]) -> Result<(), String> { + pub(crate) fn v3_record_put( + &self, + schema: &str, + key: &[u8], + value: &[u8], + ) -> Result<(), String> { self.lock_writer() .execute( "INSERT INTO schema_records (schema, record_key, value) VALUES (?1, ?2, ?3) \ @@ -370,7 +436,11 @@ impl StoreSlots for SqliteStore { .map_err(|e| e.to_string()) } - fn record_get(&self, schema: &str, key: &[u8]) -> Result, String> { + pub(crate) fn v3_record_get( + &self, + schema: &str, + key: &[u8], + ) -> Result, String> { let value: Option> = self .lock_reader() .query_row( @@ -383,7 +453,7 @@ impl StoreSlots for SqliteStore { value.map(record_bytes).transpose() } - fn record_scan( + pub(crate) fn v3_record_scan( &self, schema: &str, prefix: &[u8], @@ -432,7 +502,7 @@ impl StoreSlots for SqliteStore { .collect() } - fn reserve<'c>( + pub(crate) fn v3_reserve<'c>( &self, op: OpId, epoch: u64, @@ -448,12 +518,21 @@ impl StoreSlots for SqliteStore { |_| ReserveRefused::Unavailable, |tx| { let unavailable = |_: RecordStoreError| ReserveRefused::Unavailable; + let now = self.now_ms(); + // The epoch fences the draw (`SLICE_TTL_MS` (b)): below the stored one applies + // nothing; above it raises the stored one, in this same transaction. + let fleet = stored_epoch(tx).map_err(unavailable)?; + if epoch < fleet { + return Err(ReserveRefused::StaleEpoch); + } // The chain draw is all or nothing: test every cell against what the cells before - // it in THIS draw add, and apply only when every cell passes. + // it in THIS draw add, and apply only when every cell passes. Each slot's expired + // slices go back to its headroom first (`SLICE_TTL_MS` (c)). let mut drawn: HashMap = HashMap::new(); let mut slots = Vec::with_capacity(cells.len()); for (i, c) in cells.iter().enumerate() { let (slot, dimension) = slot_of(&c.key); + expire(tx, &slot, now).map_err(unavailable)?; let Some((cap, _)) = stored_cap(tx, &slot).map_err(unavailable)? else { return Err(ReserveRefused::NoCap { cell: i as u32 }); }; @@ -465,21 +544,31 @@ impl StoreSlots for SqliteStore { *drawn.entry(slot.clone()).or_default() += c.amount; slots.push(slot); } + if epoch > fleet { + tx.execute( + "INSERT INTO money_epoch (id, epoch) VALUES (0, ?1) \ + ON CONFLICT(id) DO UPDATE SET epoch=excluded.epoch", + params![to_db(epoch)], + ) + .store() + .map_err(unavailable)?; + } + sweep_closed(tx, now).map_err(unavailable)?; + let valid_until_ms = now.saturating_add(SLICE_TTL_MS); let mut granted = Vec::with_capacity(cells.len()); for (c, slot) in cells.iter().zip(slots) { let used = stored_used(tx, &slot).map_err(unavailable)?; set_used(tx, &slot, used.saturating_add(c.amount)).map_err(unavailable)?; let slice_id: i64 = tx .query_row( - "INSERT INTO money_slices (slot, remaining) VALUES (?1, ?2) \ - RETURNING slice_id", - params![slot, to_db(c.amount)], + "INSERT INTO money_slices (slot, remaining, valid_until_ms) \ + VALUES (?1, ?2, ?3) RETURNING slice_id", + params![slot, to_db(c.amount), to_db(valid_until_ms)], |r| r.get(0), ) .store() .map_err(unavailable)?; - // Nothing expires a slice: every node draws against the same rows. - granted.push((from_db(slice_id), c.amount, u64::MAX)); + granted.push((from_db(slice_id), c.amount, valid_until_ms)); } Ok(Answer::Grants(granted)) }, @@ -500,7 +589,7 @@ impl StoreSlots for SqliteStore { } } - fn slice_release( + pub(crate) fn v3_slice_release( &self, op: OpId, epoch: u64, @@ -510,6 +599,7 @@ impl StoreSlots for SqliteStore { let items: Vec<(u64, u64)> = items.collect(); let body = format!("slice_release:{epoch}:{items:?}"); let answer = self.op(op, &body, |tx| { + let now = self.now_ms(); let held = |id: u64| -> OpResult> { tx.query_row( "SELECT slot, remaining FROM money_slices WHERE slice_id=?1", @@ -523,31 +613,26 @@ impl StoreSlots for SqliteStore { for &(id, _) in &items { if held(id)?.is_none() { return Err(OpRefused::Failed(format!( - "slice_release: slice {id} is not held" + "slice_release: slice {id} was never granted" ))); } } let mut back_all = Vec::with_capacity(items.len()); for &(id, unspent) in &items { - let Some((slot, left)) = held(id)? else { - // An item naming a slice an EARLIER item of this call closed. - back_all.push(0); - continue; - }; + let gone = + || OpRefused::Failed(format!("slice_release: slice {id} was never granted")); + let (slot, _) = held(id)?.ok_or_else(gone)?; + // An expired slice's remainder went back to its headroom at expiry, so a release + // after it returns nothing more (`SLICE_TTL_MS`: each unit at most once). + expire(tx, &slot, now).map_err(failed)?; + let (_, left) = held(id)?.ok_or_else(gone)?; let back = unspent.min(left); - let left = left - back; - let closed = if left == 0 { - tx.execute( - "DELETE FROM money_slices WHERE slice_id=?1", - params![to_db(id)], - ) - } else { - tx.execute( - "UPDATE money_slices SET remaining=?2 WHERE slice_id=?1", - params![to_db(id), to_db(left)], - ) - }; - closed.store().map_err(failed)?; + tx.execute( + "UPDATE money_slices SET remaining=?2 WHERE slice_id=?1", + params![to_db(id), to_db(left - back)], + ) + .store() + .map_err(failed)?; let used = stored_used(tx, &slot).map_err(failed)?; set_used(tx, &slot, used.saturating_sub(back)).map_err(failed)?; back_all.push(back); @@ -563,7 +648,11 @@ impl StoreSlots for SqliteStore { } } - fn add_usage_batch(&self, op: OpId, cells: &[(&str, u64, UsageDelta)]) -> OpResult<()> { + pub(crate) fn v3_add_usage_batch( + &self, + op: OpId, + cells: &[(&str, u64, UsageDelta)], + ) -> OpResult<()> { let body = format!("add_usage_batch:{cells:?}"); self.op(op, &body, |tx| { for (bucket, window, delta) in cells { @@ -574,7 +663,7 @@ impl StoreSlots for SqliteStore { .map(drop) } - fn add_metering_batch(&self, op: OpId, deltas: &[MeteringDelta]) -> OpResult<()> { + pub(crate) fn v3_add_metering_batch(&self, op: OpId, deltas: &[MeteringDelta]) -> OpResult<()> { let body = format!("add_metering_batch:{deltas:?}"); self.op(op, &body, |tx| { for d in deltas { @@ -585,7 +674,7 @@ impl StoreSlots for SqliteStore { .map(drop) } - fn append_audit_batch(&self, op: OpId, entries: &[AuditRecord]) -> OpResult<()> { + pub(crate) fn v3_append_audit_batch(&self, op: OpId, entries: &[AuditRecord]) -> OpResult<()> { let body = format!("append_audit_batch:{entries:?}"); // One transaction: a fork anywhere (against the log, or inside the batch, which the fork // check sees because the earlier entries are already written in it) rolls back the whole. @@ -598,7 +687,7 @@ impl StoreSlots for SqliteStore { .map(drop) } - fn window_caps(&self, op: OpId, caps: &[Cap<'_>]) -> Result<(), CapsRefused> { + pub(crate) fn v3_window_caps(&self, op: OpId, caps: &[Cap<'_>]) -> Result<(), CapsRefused> { let body = format!("window_caps:{caps:?}"); self.deduped( op, @@ -641,6 +730,408 @@ impl StoreSlots for SqliteStore { } } +/// THE STORE v3 TABLE over this store (`abi::sdk::store::StoreSlots`): every slot answers +/// [`Step::Ready`]. The store is local-disk-bound and never pends, so it makes no connector service +/// and never touches [`Op`]; the host runs it on its bounded disk lane (THE DESIGN §11.11 R4, +/// Q-DISK). The 1.5.5 op set (slots 0-32) is ONE body each, the store's [`RecordStore`] impl, which +/// is exactly the code v1.0.6 shipped. +impl StoreSlots for SqliteStore { + const TAIL: Tail = Tail { + ephemeral: false, + durable_plane: true, + fork_refusal: true, + }; + + fn validate(settings: &[u8]) -> Result<(), String> { + crate::door::Settings::parse(settings_str(settings)?).map(drop) + } + + fn open(settings: &[u8], _host: Option) -> Result { + crate::door::open(settings_str(settings)?) + } + + fn add_usage_op( + &self, + _: &mut Op<'_>, + op: OpId, + bucket: &str, + window_start: u64, + delta: &UsageDelta, + ) -> Step> { + Step::Ready(self.v3_add_usage_op(op, bucket, window_start, delta)) + } + + fn add_metering_op( + &self, + _: &mut Op<'_>, + op: OpId, + delta: &MeteringDelta, + ) -> Step> { + Step::Ready(self.v3_add_metering_op(op, delta)) + } + + fn append_audit_op(&self, _: &mut Op<'_>, op: OpId, entry: &AuditRecord) -> Step> { + Step::Ready(self.v3_append_audit_op(op, entry)) + } + + fn append_plane_record_op( + &self, + _: &mut Op<'_>, + op: OpId, + record: PlaneRecordRef<'_>, + ) -> Step> { + Step::Ready(self.v3_append_plane_record_op(op, record)) + } + + fn append_batch( + &self, + _: &mut Op<'_>, + op: OpId, + stream: &str, + records: &[RecordBytes], + ) -> Step> { + Step::Ready(self.v3_append_batch(op, stream, records)) + } + + fn heads(&self, _: &mut Op<'_>) -> Step, String>> { + Step::Ready(self.v3_heads()) + } + + fn session_put( + &self, + _: &mut Op<'_>, + session: u64, + node: &str, + principal: &str, + ) -> Step> { + Step::Ready(self.v3_session_put(session, node, principal)) + } + + fn session_remove(&self, _: &mut Op<'_>, session: u64) -> Step> { + Step::Ready(self.v3_session_remove(session)) + } + + fn sessions_for( + &self, + _: &mut Op<'_>, + principal: &str, + ) -> Step, String>> { + Step::Ready(self.v3_sessions_for(principal)) + } + + fn record_put( + &self, + _: &mut Op<'_>, + schema: &str, + key: &[u8], + value: &[u8], + ) -> Step> { + Step::Ready(self.v3_record_put(schema, key, value)) + } + + fn record_get( + &self, + _: &mut Op<'_>, + schema: &str, + key: &[u8], + ) -> Step, String>> { + Step::Ready(self.v3_record_get(schema, key)) + } + + fn record_scan( + &self, + _: &mut Op<'_>, + schema: &str, + prefix: &[u8], + limit: u32, + ) -> Step> { + Step::Ready(self.v3_record_scan(schema, prefix, limit)) + } + + fn reserve<'c>( + &self, + _: &mut Op<'_>, + op: OpId, + epoch: u64, + cells: impl Iterator> + Clone, + grants: &mut impl Extend, + ) -> Step> { + Step::Ready(self.v3_reserve(op, epoch, cells, grants)) + } + + fn slice_release( + &self, + _: &mut Op<'_>, + op: OpId, + epoch: u64, + items: impl Iterator + Clone, + released: &mut impl Extend, + ) -> Step> { + Step::Ready(self.v3_slice_release(op, epoch, items, released)) + } + + fn add_usage_batch( + &self, + _: &mut Op<'_>, + op: OpId, + cells: &[(&str, u64, UsageDelta)], + ) -> Step> { + Step::Ready(self.v3_add_usage_batch(op, cells)) + } + + fn add_metering_batch( + &self, + _: &mut Op<'_>, + op: OpId, + deltas: &[MeteringDelta], + ) -> Step> { + Step::Ready(self.v3_add_metering_batch(op, deltas)) + } + + fn append_audit_batch( + &self, + _: &mut Op<'_>, + op: OpId, + entries: &[AuditRecord], + ) -> Step> { + Step::Ready(self.v3_append_audit_batch(op, entries)) + } + + fn window_caps( + &self, + _: &mut Op<'_>, + op: OpId, + caps: &[Cap<'_>], + ) -> Step> { + Step::Ready(self.v3_window_caps(op, caps)) + } + + // ── the 1.5.5 op set (slots 0-32): the RecordStore impl, one body each ─────────────────────── + + fn put_key(&self, _: &mut Op<'_>, key: &VirtualKey) -> Step> { + Step::Ready(RecordStore::put_key(self, key)) + } + + fn get_key(&self, _: &mut Op<'_>, id: &str) -> Step>> { + Step::Ready(RecordStore::get_key(self, id)) + } + + fn list_keys(&self, _: &mut Op<'_>) -> Step>> { + Step::Ready(RecordStore::list_keys(self)) + } + + fn delete_key(&self, _: &mut Op<'_>, id: &str) -> Step> { + Step::Ready(RecordStore::delete_key(self, id)) + } + + fn scrub_key(&self, _: &mut Op<'_>, id: &str) -> Step> { + Step::Ready(RecordStore::scrub_key(self, id)) + } + + fn list_keys_since( + &self, + _: &mut Op<'_>, + since: u64, + ) -> Step>> { + Step::Ready(RecordStore::list_keys_since(self, since)) + } + + fn get_usage( + &self, + _: &mut Op<'_>, + bucket_id: &str, + window_start: u64, + ) -> Step> { + Step::Ready(RecordStore::get_usage(self, bucket_id, window_start)) + } + + fn put_usage( + &self, + _: &mut Op<'_>, + bucket_id: &str, + window_start: u64, + ledger: &UsageLedger, + ) -> Step> { + Step::Ready(RecordStore::put_usage( + self, + bucket_id, + window_start, + ledger, + )) + } + + fn list_metering( + &self, + _: &mut Op<'_>, + bucket: u64, + ) -> Step>> { + Step::Ready(RecordStore::list_metering(self, bucket)) + } + + fn purge_windows_before(&self, _: &mut Op<'_>, before: u64) -> Step> { + Step::Ready(RecordStore::purge_windows_before(self, before)) + } + + fn purge_metering_before(&self, _: &mut Op<'_>, bucket: &str) -> Step> { + Step::Ready(RecordStore::purge_metering_before(self, bucket)) + } + + fn put_credential( + &self, + _: &mut Op<'_>, + secret: &CredentialSecret, + ) -> Step> { + Step::Ready(RecordStore::put_credential(self, secret)) + } + + fn put_key_with_credential( + &self, + _: &mut Op<'_>, + key: &VirtualKey, + secret: &CredentialSecret, + ) -> Step> { + Step::Ready(RecordStore::put_key_with_credential(self, key, secret)) + } + + fn list_credentials( + &self, + _: &mut Op<'_>, + key_id: &str, + ) -> Step>> { + Step::Ready(RecordStore::list_credentials(self, key_id)) + } + + fn lookup_credential_secret( + &self, + _: &mut Op<'_>, + kind: &str, + public_id: &str, + ) -> Step>> { + Step::Ready(RecordStore::lookup_credential_secret(self, kind, public_id)) + } + + fn revoke_credential( + &self, + _: &mut Op<'_>, + id: &str, + reason: &str, + ) -> Step> { + Step::Ready(RecordStore::revoke_credential(self, id, reason)) + } + + fn list_credentials_since( + &self, + _: &mut Op<'_>, + since: u64, + ) -> Step>> { + Step::Ready(RecordStore::list_credentials_since(self, since)) + } + + fn list_audit(&self, _: &mut Op<'_>) -> Step>> { + Step::Ready(RecordStore::list_audit(self)) + } + + fn add_denylist(&self, _: &mut Op<'_>, sub: &str, reason: &str) -> Step> { + Step::Ready(RecordStore::add_denylist(self, sub, reason)) + } + + fn list_denylist(&self, _: &mut Op<'_>) -> Step>> { + Step::Ready(RecordStore::list_denylist(self)) + } + + fn list_audit_tail( + &self, + _: &mut Op<'_>, + limit: u64, + ) -> Step>> { + Step::Ready(RecordStore::list_audit_tail(self, limit)) + } + + fn upsert_plane_record( + &self, + _: &mut Op<'_>, + record: PlaneRecordRef<'_>, + ) -> Step> { + Step::Ready(RecordStore::upsert_plane_record(self, record)) + } + + fn get_plane_record( + &self, + _: &mut Op<'_>, + kind: &str, + id: &str, + ) -> Step>>> { + Step::Ready(RecordStore::get_plane_record(self, kind, id)) + } + + fn list_plane_records( + &self, + _: &mut Op<'_>, + kind: &str, + selector: &PlaneSelector<'_>, + ) -> Step>>> { + Step::Ready(RecordStore::list_plane_records(self, kind, selector)) + } + + fn list_plane_record_parents( + &self, + _: &mut Op<'_>, + kind: &str, + ) -> Step>> { + Step::Ready(RecordStore::list_plane_record_parents(self, kind)) + } + + fn purge_plane_records_before( + &self, + _: &mut Op<'_>, + kind: &str, + before: u64, + ) -> Step> { + Step::Ready(RecordStore::purge_plane_records_before(self, kind, before)) + } + + fn delete_plane_record( + &self, + _: &mut Op<'_>, + kind: &str, + id: &str, + ) -> Step> { + Step::Ready(RecordStore::delete_plane_record(self, kind, id)) + } + + fn redeem_plane_token( + &self, + _: &mut Op<'_>, + kind: &str, + token: &str, + expires_at: u64, + now: u64, + ) -> Step> { + Step::Ready(RecordStore::redeem_plane_token( + self, kind, token, expires_at, now, + )) + } + + fn plane_token_live( + &self, + _: &mut Op<'_>, + kind: &str, + token: &str, + expires_at: u64, + now: u64, + ) -> Step> { + Step::Ready(RecordStore::plane_token_live( + self, kind, token, expires_at, now, + )) + } +} + +/// The operator's settings as the text the door's parser reads. +fn settings_str(settings: &[u8]) -> Result<&str, String> { + std::str::from_utf8(settings) + .map_err(|e| format!("invalid sqlite plugin config: not UTF-8: {e}")) +} + /// A stored record's bytes as the contract's bounded record. fn record_bytes(v: Vec) -> Result { RecordBytes::new(v).map_err(|n| format!("a stored record of {n} bytes is over the ceiling"))