From c84e310a74401a068852a00a2c0661e1163130b4 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sat, 3 Oct 2026 04:11:11 -0700 Subject: [PATCH 01/16] store: Postgres over the host's connector (no own socket; ticketed open) ARCHITECT rulings 2026-10-03 on Q-L14-1 and Q-L16-2 (busbar THE DESIGN: every call Ready or Pending(wake); no plugin opens its own socket). Depends on busbar lane-dg-storebridge@c7cc4f6696. - The logic crate drops the `postgres` driver. src/pgwire.rs is an async Postgres frontend on the sans-IO postgres-protocol (messages, SCRAM-SHA-256, md5) and postgres-types (the same binary ToSql/FromSql the 1.5.5 driver used), every byte over the op's one connection (store SDK wire::drive / Op::checkout). Parse/Describe/Sync then Bind/Execute/Sync, binary values; an uncommitted Transaction is rolled back before the connection's next statement. - store_door!(.., needs: NEEDS): one outbound `tcp` need, operator-infrastructure, target = the DSN's host:port. `open` parses the settings (same refusal texts); StoreSlots::connect connects, authenticates and migrates, so an unreachable or refusing server still fails the load at boot in the driver's words ("error connecting to server: ...", password scrubbed). - Every op is one connection: startup + auth, the 1.5.5 SQL body (now on a Session, with .await), Terminate. RepeatableRead snapshots, advisory locks, SQLSTATE checks, render_pg_error/scrub and the durable op_id dedupe are unchanged. sslmode=require|verify-* -> SSLRequest + upgrade_secure through the host; the other modes stay plaintext as 1.5.5's NoTls. - The plugin crate exports only the door; the cold-lane registration is deleted. - busbar pinned at c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8 (manifests, .busbar-ref, workflows). Tests open the store through the real loader with busbar's test connection table (tcp_conns), raw verification on an independent `postgres` connection (dev-dependency). The metering test moves to bucket 20_270_611: it raced the purge test on 20_270_601. --- .busbar-ref | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/consumer-verify.yml | 2 +- .github/workflows/release.yml | 4 +- .github/workflows/repin.yml | 2 +- Cargo.lock | 325 ++-- Cargo.toml | 4 +- README.md | 21 +- store-postgres-plugin/Cargo.toml | 2 +- store-postgres-plugin/src/lib.rs | 26 +- store-postgres-plugin/tests/admin_api_e2e.rs | 2 +- store-postgres-plugin/tests/common/mod.rs | 38 +- store-postgres-plugin/tests/e2e.rs | 33 +- store-postgres/Cargo.toml | 14 +- store-postgres/src/lib.rs | 590 +++++--- store-postgres/src/pgwire.rs | 1128 ++++++++++++++ store-postgres/src/tests.rs | 122 +- store-postgres/src/tests/harness.rs | 277 ++++ store-postgres/src/tests/plane_records.rs | 14 +- store-postgres/src/tests/v160_shapes.rs | 5 +- store-postgres/src/tests/v3_slots.rs | 12 +- store-postgres/src/v3.rs | 1389 ++++++++++++++---- 22 files changed, 3131 insertions(+), 883 deletions(-) create mode 100644 store-postgres/src/pgwire.rs create mode 100644 store-postgres/src/tests/harness.rs diff --git a/.busbar-ref b/.busbar-ref index 3e77816..7514538 100644 --- a/.busbar-ref +++ b/.busbar-ref @@ -1 +1 @@ -e1d3d8b097201859751d77208e66249d98b50a4b 1.6.0 +c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8 1.6.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce33fea..8acda69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ permissions: jobs: ci: - uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@e1d3d8b097201859751d77208e66249d98b50a4b + uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8 with: service: postgres busbar_checkout: true diff --git a/.github/workflows/consumer-verify.yml b/.github/workflows/consumer-verify.yml index 1e7b94a..4d77561 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@c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8 with: version: ${{ inputs.version || '' }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d96335..bb19ac5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: secrets: inherit release: needs: ci - uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@e1d3d8b097201859751d77208e66249d98b50a4b + uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8 with: plugin_crate: busbar-store-postgres-plugin manifest_name: busbar-store-postgres @@ -41,7 +41,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@c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8 with: version: ${{ github.ref_name }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/repin.yml b/.github/workflows/repin.yml index bbc104c..ddb0a78 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@c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8 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 a82b003..e172ad9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "async-trait" -version = "0.1.92" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", @@ -39,9 +39,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "block-buffer" @@ -54,9 +54,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] @@ -70,9 +70,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=c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8#c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8" dependencies = [ "async-trait", + "base64", "futures", "hex", "http", @@ -81,7 +82,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml_ng", - "sha2 0.11.0", + "sha2 0.10.9", "smallvec", "tracing", "tracing-core", @@ -92,28 +93,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=c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8#c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8" 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=c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8#c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8" 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=c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8#c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8" dependencies = [ - "async-trait", "busbar-contract", "busbar-kernel-ledger", "busbar-kernel-wal", @@ -126,7 +126,6 @@ dependencies = [ "libloading", "serde", "serde_json", - "sha2 0.11.0", "tar", "tokio", "tracing", @@ -137,7 +136,12 @@ name = "busbar-store-postgres" version = "1.0.0" dependencies = [ "busbar-contract", + "busbar-plugin-loader", + "bytes", + "fallible-iterator", "postgres", + "postgres-protocol", + "postgres-types", "serde", "serde_json", ] @@ -169,9 +173,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.2.62" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "shlex", @@ -185,9 +189,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" @@ -238,9 +242,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -297,7 +301,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -326,7 +330,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", @@ -334,13 +338,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -373,16 +377,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.61.2", -] - [[package]] name = "fallible-iterator" version = "0.2.0" @@ -413,9 +407,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.10" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -432,9 +426,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -447,9 +441,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -457,15 +451,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -474,38 +468,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -590,9 +584,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -610,9 +604,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.5" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -629,18 +623,18 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.11.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -824,13 +818,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -852,19 +845,13 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.25" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "libc", ] -[[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" @@ -882,9 +869,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.30" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru-slab" @@ -904,15 +891,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miniz_oxide" -version = "0.9.1" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", @@ -920,9 +907,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -1071,18 +1058,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -1122,9 +1109,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", @@ -1136,9 +1123,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -1229,14 +1216,12 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", - "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", "web-sys", "webpki-roots", ] @@ -1257,9 +1242,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -1270,24 +1255,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.61.2", -] - [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "once_cell", "ring", @@ -1309,9 +1281,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -1320,9 +1292,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -1434,9 +1406,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signature" @@ -1455,9 +1427,9 @@ checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "siphasher" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -1467,15 +1439,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -1516,9 +1488,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -1553,7 +1525,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1564,27 +1536,26 @@ checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", - "xattr", ] [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -1599,9 +1570,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -1654,9 +1625,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.5" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -1664,13 +1635,14 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] @@ -1739,7 +1711,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1870,9 +1842,9 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] @@ -1888,9 +1860,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -1901,9 +1873,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.72" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -1911,9 +1883,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1921,44 +1893,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "web-sys" -version = "0.3.99" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -1976,18 +1935,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] [[package]] name = "whoami" -version = "2.1.3" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" dependencies = [ "libc", "libredox", @@ -2096,21 +2055,11 @@ 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.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -2125,7 +2074,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "synstructure", ] @@ -2146,7 +2095,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "synstructure", ] @@ -2186,11 +2135,11 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index cb33e47..1654eec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,8 +29,8 @@ publish = false repository = "https://github.com/GetBusbar/busbar-store-postgres" [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 = "c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8" } +busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8" } # busbar's root [workspace.dependencies], third-party rows, at e1d3d8b097201859751d77208e66249d98b50a4b a2a-lf = "0.3.0" a2a-pb = "0.2.0" diff --git a/README.md b/README.md index 8587016..2edc954 100644 --- a/README.md +++ b/README.md @@ -97,13 +97,18 @@ the engine's JSON config (`{"url": "postgres://..."}`) into a ### Known limitations (documented honestly, not papered over) -- **No TLS in this build (`NoTls`).** Run the connection over a trusted - network segment, a local socket, or a TLS-terminating proxy - (pgbouncer/stunnel). -- **No automatic reconnect.** A persistently dropped connection - surfaces as store errors on the write-behind flush path and on admin - operations; a permanently broken connection requires a process - restart (let your supervisor handle it). +- **The store opens no socket of its own.** Every operation reaches + Postgres through busbar's connector (the store declares one outbound + `tcp` need in the `operator-infrastructure` egress class) as its own + connection: connect, authenticate, run, close. A dropped connection + fails that one operation; the next one connects afresh. +- **One connection per operation.** There is no pool yet, so every + operation pays a connect and an authentication. +- **TLS through busbar.** `sslmode=require`, `verify-ca` and + `verify-full` secure the connection through busbar's connector and its + trust anchors; `disable`, `allow` and `prefer` connect in plaintext, + as 1.5.x (`NoTls`) did. A Unix-socket `host` is refused: the store + reaches its server over TCP. See the doc comments at the top of [`store-postgres/src/lib.rs`](store-postgres/src/lib.rs) @@ -115,7 +120,7 @@ thin `cdylib` adapter around it. | Setting | Required | Default | Notes | |---|---|---|---| -| `url` | yes | — | A libpq connection string, e.g. `postgres://user:pass@host:5432/busbar`. Connects `NoTls`; run it over a trusted network segment or a TLS-terminating proxy. **No connect timeout is set by default** — a blackholed host wedges engine boot indefinitely. libpq honors a `connect_timeout` query param in the DSN, e.g. `postgres://user:pass@host:5432/busbar?connect_timeout=10`; set one if boot hanging on a dead host is a concern. | +| `url` | yes | — | A libpq connection string, URL (`postgres://user:pass@host:5432/busbar?sslmode=require`) or keyword form (`host=... user=... password=... dbname=...`). Connections go through busbar's connector, whose deadlines bound every connect (`connect_timeout` is accepted and ignored). `sslmode=require`/`verify-*` secures the connection through busbar; the other modes are plaintext. | ## Build diff --git a/store-postgres-plugin/Cargo.toml b/store-postgres-plugin/Cargo.toml index 798dfad..728266a 100644 --- a/store-postgres-plugin/Cargo.toml +++ b/store-postgres-plugin/Cargo.toml @@ -28,7 +28,7 @@ busbar-contract = { workspace = true } # The conformance and end-to-end tests load the built cdylib over the REAL loader (sign, pack, scan, # open) and the linked row through the same registry — 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 = { workspace = true } +busbar-plugin-loader = { workspace = true, features = ["test-support"] } busbar-contract = { workspace = true } serde_json = { workspace = true } postgres = { workspace = true } diff --git a/store-postgres-plugin/src/lib.rs b/store-postgres-plugin/src/lib.rs index ae3899a..050f936 100644 --- a/store-postgres-plugin/src/lib.rs +++ b/store-postgres-plugin/src/lib.rs @@ -10,8 +10,10 @@ //! All the store lives in the `busbar-store-postgres` crate, including its one door (`door`, from //! `store_door!`). This crate re-exports the logic crate so the library it builds carries exactly //! the code a busbar build that links the store runs — one source, both doors (DECISIONS #2 rule -//! (1)) — and exports that same `door` as `busbar_plugin_door`. It also registers the store on the -//! cold store lane the busbar kernel at the pin boots a configured store through (`cold`). +//! (1)) — and exports that same `door` as `busbar_plugin_door`. +//! +//! The library opens no socket: the store reaches its server through the host's connector (its +//! one declared `tcp` need), and the image exports exactly the one door symbol. #![deny(unsafe_code)] @@ -22,23 +24,3 @@ pub use busbar_store_postgres::*; mod exported { busbar_contract::export_door!(busbar_store_postgres::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 -/// [`PostgresStore`], opened by the door's own `open` slot (same settings, same refusals). -fn open_cold(cfg: &str) -> Result { - use busbar_contract::abi::sdk::store::StoreSlots; - ::open(cfg.as_bytes()) - .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. -#[allow(unsafe_code)] -mod cold { - busbar_contract::abi::sdk::export_store_plugin!(super::open_cold); -} diff --git a/store-postgres-plugin/tests/admin_api_e2e.rs b/store-postgres-plugin/tests/admin_api_e2e.rs index 280310d..043ef39 100644 --- a/store-postgres-plugin/tests/admin_api_e2e.rs +++ b/store-postgres-plugin/tests/admin_api_e2e.rs @@ -29,7 +29,7 @@ //! 5. Boot a SECOND real `busbar` process against that config. Busbar's own first-boot store //! resolution (`plugin_registry.open_store`, called synchronously during construction, before //! the listener ever binds) dlopens the plugin that landed on disk via the admin API in step 2. -//! Poll — via a RAW independent `postgres::Client`, never `PostgresStore::connect` — for the +//! Poll — via a RAW independent `postgres::Client`, never the store itself — for the //! `keys` table to appear, proving the real dlopen + `Store::connect`/`migrate()` path executed. //! 6. `POST /api/v1/admin/keys` (with `issue_aws_credential: true`) against this SECOND process — //! REAL WORK: mint a virtual key AND a credential through the running instance, over the same diff --git a/store-postgres-plugin/tests/common/mod.rs b/store-postgres-plugin/tests/common/mod.rs index 2c9e065..d5cb445 100644 --- a/store-postgres-plugin/tests/common/mod.rs +++ b/store-postgres-plugin/tests/common/mod.rs @@ -17,6 +17,7 @@ use busbar_plugin_loader::dispatch::{ load_dropped, load_linked, rendering_of, Bind, DispatchConfig, Dispatcher, LinkedRow, NoSink, }; use busbar_plugin_loader::store_v3::LoadedStore; +use busbar_plugin_loader::tcp_conns::TcpConns; /// This crate's built cdylib (uplifted or under `deps`, newest wins). A missing artifact is a /// failure, never a skip: the dropped-in door is what these tests prove. @@ -41,28 +42,35 @@ pub fn stated() -> Vec { rendering_of(busbar_store_postgres::door).expect("the store renders its Statement") } -/// A node id no other open (in this run or an earlier one) has used. The store dedupes `op_id`s -/// DURABLY and the `LoadedStore` bridge mints them as `(node, counter)` from 0, so two opens -/// sharing a node id against one database would replay or refuse each other's writes; a kernel's -/// node id is unique per node, and this stands in for it. -fn node() -> u64 { +/// A fresh `op_id` for every bridge write: a node half no other open (in this run or an earlier +/// one) used. The store dedupes `op_id`s DURABLY, so two opens sharing a node half against one +/// database would replay or refuse each other's writes; a kernel's node id is unique per node, and +/// this stands in for it. +fn mint() -> busbar_contract::abi::store::OpId { + static NODE: std::sync::OnceLock = std::sync::OnceLock::new(); static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); - let nanos = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() as u64; - nanos - ^ (u64::from(std::process::id()) << 40) - ^ N.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + let node = *NODE.get_or_init(|| { + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + nanos ^ (u64::from(std::process::id()) << 40) + }); + busbar_contract::abi::store::OpId::from_parts( + node, + N.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1, + ) } +/// The instance's binding: its connections over the loader's test connection table (plain TCP), +/// woken through the dispatcher, as busbar's one connector serves a store's `tcp` need. fn bind(d: &Dispatcher) -> Bind { Bind { instance: Arc::from("store-postgres-test"), max_inflight_cap: 64, sink: Arc::new(NoSink), dispatcher: d.adopter(), - conns: None, + conns: Some(Arc::new(TcpConns::new(d.conn_waker()))), } } @@ -71,7 +79,7 @@ pub fn linked(settings: &str) -> Result { let d = Arc::new(Dispatcher::new(DispatchConfig::default())); let row = LinkedRow::of(busbar_store_postgres::door).map_err(|e| e.to_string())?; let p = load_linked::(&row, bind(&d)).map_err(|e| e.to_string())?; - LoadedStore::open(p, d, settings.as_bytes(), node()) + LoadedStore::open(p, d, settings.as_bytes(), mint) } /// The library at `path` through the DROPPED-IN door, admitted against [`stated`] and opened on @@ -79,7 +87,7 @@ pub fn linked(settings: &str) -> Result { pub fn dropped_at(path: &Path, settings: &str) -> Result { let d = Arc::new(Dispatcher::new(DispatchConfig::default())); let p = load_dropped::(path, &stated(), bind(&d)).map_err(|e| e.to_string())?; - LoadedStore::open(p, d, settings.as_bytes(), node()) + LoadedStore::open(p, d, settings.as_bytes(), mint) } /// This crate's cdylib through the DROPPED-IN door. diff --git a/store-postgres-plugin/tests/e2e.rs b/store-postgres-plugin/tests/e2e.rs index 0457421..146d574 100644 --- a/store-postgres-plugin/tests/e2e.rs +++ b/store-postgres-plugin/tests/e2e.rs @@ -24,14 +24,14 @@ //! Persistence is then proven the same two independent ways the prior direct-call test used (kept — //! this part was always sound, only the LOADING mechanism was wrong): //! 1. The boot runs against a DISPOSABLE, freshly created, genuinely empty database, and is polled -//! via a RAW independent `postgres::Client` connection (never `PostgresStore::connect`, so this +//! via a RAW independent `postgres::Client` connection (never the store itself, so this //! check can't create the schema itself) for the `keys` table to appear within a timeout. The //! empty database is what makes this a proof rather than a formality: against the shared test //! database, every live unit test in this workspace has already migrated a `keys` table into //! existence, so the same poll would break true on its first iteration even if `busbar` had //! failed to load the plugin and exited immediately. Then the schema VERSION the boot wrote is //! read back, and the child is confirmed still running rather than having died after migrating. -//! 2. Only AFTER those proofs, a second, independent `PostgresStore::connect` (bypassing the +//! 2. Only AFTER those proofs, a second, independent open of the LINKED door (bypassing the //! plugin/ABI/loader entirely) confirms the store type itself can talk to the same schema. //! //! The two ABI-contract error-path tests below (`bad_config_fails_over_abi`, `refuses_non_plugin`) @@ -43,7 +43,6 @@ mod common; -use busbar_store_postgres::PostgresStore; use std::path::PathBuf; use std::process::{Child, Command, Stdio}; use std::time::{Duration, Instant}; @@ -484,7 +483,7 @@ fn load_and_exercise_postgres_plugin_via_file_drop() { // REAL BOOT: run the actual gateway process (no --validate) against the same file-dropped // plugin + config, and poll -- via a RAW independent postgres::Client connection, never - // PostgresStore::connect, so this check can't accidentally create the schema itself -- for the + // the store, so this check can't accidentally create the schema itself -- for the // `keys` table to appear. This is the only genuine proof that boot actually dlopened the plugin // and called Store::connect (which runs migrate()) before ever handling a request, and it is a // proof only because the database above was created empty for this test alone: against the @@ -541,7 +540,7 @@ fn load_and_exercise_postgres_plugin_via_file_drop() { // The boot did not just create a table, it landed the CURRENT schema: read the version the // plugin-loaded migrate() wrote, over a raw client. Checked before the direct connect below, - // because `PostgresStore::connect` runs migrate() itself and would write this row if the boot + // because the store's connect step runs migrate() itself and would write this row if the boot // had not. let mut raw = postgres::Client::connect(&url, postgres::NoTls) .expect("raw connect to the fresh database"); @@ -562,10 +561,10 @@ fn load_and_exercise_postgres_plugin_via_file_drop() { guard.output() ); - // Only AFTER those proofs: a second, independent PostgresStore::connect (bypassing the + // Only AFTER those proofs: a second, independent open of the LINKED door (bypassing the // plugin/ABI/loader entirely) confirms the store type itself can talk to the same schema the // real boot process just created. - let _direct = PostgresStore::connect(&url).expect( + let _direct = common::linked(&cfg(&url)).expect( "connect directly, bypassing the plugin entirely, to confirm the schema the real boot \ created is usable", ); @@ -642,7 +641,7 @@ fn plane_decode(b: &[u8]) -> serde_json::Value { /// cdylib, the real store v3 table, the real `LoadedStore`. It writes AT ARITY > 1 (three chained /// records for one principal and one for a second), DROPS the handle — which closes the instance, so /// nothing this process still holds can answer the reads — then `dlopen`s AGAIN over the same file -/// and reads everything back. A third leg reads the same rows through the plain `PostgresStore`, +/// and reads everything back. A third leg reads the same rows through the linked door, /// never touching the cdylib, the door or the loader — so a plugin that answered from its own /// in-process cache still fails here. #[test] @@ -660,7 +659,7 @@ fn mcp_call_log_survives_an_unload_and_reload_over_the_real_plugin_abi() { // assertions below meaningless. A purge at the top of the range is the store's own // contract-level wipe, so this needs no raw-SQL knowledge of the schema. No other test in this // binary touches the `call` kind. - let direct = PostgresStore::connect(&url).expect("connect directly to clean up and verify"); + let direct = common::linked(&cfg).expect("connect directly to clean up and verify"); RecordStore::purge_plane_records_before(&direct, "call", i64::MAX as u64) .expect("wipe the call log before this run"); @@ -800,8 +799,8 @@ fn mcp_call_log_survives_an_unload_and_reload_over_the_real_plugin_abi() { ); drop(store); - // LEG 3 — read the surviving rows through the plain `PostgresStore`, a code path that never - // touches the cdylib, the door or the loader. + // LEG 3 — read the surviving rows through the LINKED door (`common::linked`), a code path + // that never touches the cdylib or its dlopen. let direct_calls = chain(&direct, &p_main); assert_eq!( direct_calls @@ -828,7 +827,7 @@ fn mcp_call_log_survives_an_unload_and_reload_over_the_real_plugin_abi() { /// /// A REAL `dlopen`, the real store v3 table, the real `LoadedStore`. Write at arity > 1 (two tasks, one of them /// UPSERTED a second time, plus two independent provenance chains), DROP the handle, `dlopen` again -/// and read everything back; a third leg reads through the plain `PostgresStore`. +/// and read everything back; a third leg reads through the linked door. #[test] fn task_store_survives_an_unload_and_reload_over_the_real_plugin_abi() { use busbar_contract::records::{PlaneDisposition, PlaneRecord, PlaneSelector, RecordStore}; @@ -842,7 +841,7 @@ fn task_store_survives_an_unload_and_reload_over_the_real_plugin_abi() { // Start from no TERMINAL tasks. The task purge is GLOBAL and terminal-only, and its count is // asserted exactly below, so a leftover terminal row from an earlier run would make that // assertion meaningless. The store's own contract-level sweep of exactly that population. - let direct = PostgresStore::connect(&url).expect("connect directly to clean up and verify"); + let direct = common::linked(&cfg).expect("connect directly to clean up and verify"); RecordStore::purge_plane_records_before(&direct, "task", i64::MAX as u64) .expect("wipe terminal tasks before this run"); @@ -1042,7 +1041,7 @@ fn task_store_survives_an_unload_and_reload_over_the_real_plugin_abi() { ); drop(store); - // LEG 3 — the surviving rows through the plain `PostgresStore`. + // LEG 3 — the surviving rows through the linked door. let direct_task = RecordStore::get_plane_record(&direct, "task", &t_live) .expect("get_plane_record via the direct connection") .expect("the task must be physically present in Postgres, not just cached in-process"); @@ -1075,7 +1074,7 @@ fn task_store_survives_an_unload_and_reload_over_the_real_plugin_abi() { /// written, reported successful and DISCARDED (a restart hands a quarantined upstream the operator's /// approval back), and every redeemer of one single-use approval is told the trait default's answer. /// Two simultaneous loads are the fleet; a drop and a reload is the restart; a third leg reads -/// through the plain `PostgresStore`. +/// through the linked door. /// /// PANICS rather than skipping when no Postgres is configured: these are the only over-the-ABI /// coverage of two properties whose unimplemented form is silently green. @@ -1201,8 +1200,8 @@ fn trust_state_survives_an_unload_and_reload_over_the_real_plugin_abi() { "a freshly minted approval is not the one that was spent" ); - // LEG 3 — the same rows through the plain `PostgresStore`. - let direct = PostgresStore::connect(&url).expect("connect directly to verify and clean up"); + // LEG 3 — the same rows through the linked door. + let direct = common::linked(&cfg).expect("connect directly to verify and clean up"); assert!( demotions(&direct) .iter() diff --git a/store-postgres/Cargo.toml b/store-postgres/Cargo.toml index a8dfda8..a615ff5 100644 --- a/store-postgres/Cargo.toml +++ b/store-postgres/Cargo.toml @@ -14,7 +14,14 @@ license = "Apache-2.0" # 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 = { workspace = true } -postgres = { workspace = true } +# The Postgres frontend protocol WITHOUT a socket (busbar THE DESIGN, the connections section: the +# store reaches its server only over the host's connector): the message codec, SCRAM-SHA-256 and md5 +# (`postgres-protocol`) and the binary value encodings (`postgres-types`), the same two crates the +# 1.5.5 `postgres` driver encoded and decoded with, so every parameter and column reads as before. +postgres-protocol = "0.6" +postgres-types = "0.2" +bytes = { workspace = true } +fallible-iterator = "0.2" serde_json = { workspace = true } [dev-dependencies] @@ -24,3 +31,8 @@ serde_json = { workspace = true } # Its plane-record checks serialize stand-in rows with serde derive, hence this one dev-dependency; # no busbar crate beyond `busbar-contract` is needed. serde = { workspace = true } +# The live tests open the store through the REAL loader on a dispatcher, its connections over the +# loader's test connection table (`tcp_conns`, `test-support`), and verify on an independent +# connection of the `postgres` driver's own. +busbar-plugin-loader = { workspace = true, features = ["test-support"] } +postgres = { workspace = true } diff --git a/store-postgres/src/lib.rs b/store-postgres/src/lib.rs index 7d6775f..954aa23 100644 --- a/store-postgres/src/lib.rs +++ b/store-postgres/src/lib.rs @@ -2,8 +2,11 @@ // Copyright (C) 2026 Busbar Inc and contributors //! The **Postgres** backend for busbar's durable governance store — the shared, multi-node `db` -//! plugin. Implements `busbar_contract::records::RecordStore` over a mutex-guarded synchronous `postgres` client, -//! depending only on the `busbar-contract` crate (plus the `postgres` driver), never on the engine. +//! plugin. Serves the store kind's table (`StoreSlots`) over the HOST'S CONNECTOR: every op is one +//! connection, opened, authenticated and closed inside the op as straight-line async code (`pgwire`, +//! the Postgres frontend protocol over the store SDK's `wire`), so the store opens no socket of its +//! own and no op blocks a thread. Depends only on the `busbar-contract` crate (plus the sans-IO +//! `postgres-protocol`/`postgres-types` codec), never on the engine. //! //! Served through the store kind's ONE door (`store_door!` in `v3`, the store v3 table of busbar //! 1.6.0): linked into a busbar build as `door`, or dropped in as the sibling plugin cdylib. @@ -24,15 +27,19 @@ //! credentials in another would let a `REPEATABLE READ` hydration snapshot land between them and //! observe a "deleted" key whose credential is still live). //! -//! Like the prior schema, this is a **single mutex-guarded connection** used off the request hot -//! path (key CRUD + the write-behind usage flush) — governance is off the reactor entirely. +//! CONNECTIONS. The 1.5.x store held one mutex-guarded connection for its life; this one holds +//! none. Each op dials through the host (its one declared `tcp` need, `operator-infrastructure`), +//! authenticates (SCRAM-SHA-256, md5 or cleartext, as the server asks) and runs its 1.5.5 SQL body +//! on that connection; the schema is ensured once, by `open`'s connect step, so an unreachable or +//! refusing server still fails the load at boot, in the driver's words. //! //! ## Known limitations (documented honestly, not papered over) //! -//! - **No TLS in this build (`NoTls`).** Run the connection over a trusted network segment, a local -//! socket, or a TLS-terminating proxy (pgbouncer/stunnel). -//! - **No automatic reconnect.** A persistently dropped connection surfaces as store errors; a -//! permanently broken connection requires a process restart. +//! - **TLS through the host.** `sslmode=require` / `verify-ca` / `verify-full` asks the server for +//! TLS and secures the connection through the host's connector (its trust anchors); `disable`, +//! `allow` and `prefer` connect in plaintext, as the 1.5.x build (`NoTls`) did. +//! - **One connection per op.** No pool yet (the host's `checkout`/`checkin` are not offered), so +//! every op pays a connect and an authentication. //! - **No partitioning, no LISTEN/NOTIFY-accelerated hydration, no column-level secret grants in //! this pass.** The design session that produced this schema recommended all three as scale/perf //! layers on top of this contract — deliberately deferred here in favor of getting the @@ -43,24 +50,25 @@ #![forbid(unsafe_code)] +mod pgwire; + use busbar_contract::records::{ AuditRecord, CredentialMeta, CredentialSecret, MeteringDelta, MeteringRow, ModelTokens, - PlaneDisposition, PlaneRecord, PlaneRecordRef, PlaneSelector, RecordStore, RecordStoreError, + PlaneDisposition, PlaneRecord, PlaneRecordRef, PlaneSelector, RecordStoreError, RecordStoreResult, ScopeRef, SecretForm, UsageDelta, UsageLedger, VirtualKey, UNIT_CACHE_READ, UNIT_CACHE_WRITE, UNIT_INPUT, UNIT_OUTPUT, }; -use postgres::types::ToSql; -use postgres::{Client, NoTls, Row, Transaction}; +use pgwire::{Client, Row, Transaction}; +use postgres_types::ToSql; use std::collections::BTreeMap; use std::sync::atomic::AtomicU64; -use std::sync::Mutex; // postgres driver error -> the api's backend-agnostic `RecordStoreError` (the contract crate stays // storage-free, so the `From` impl that powers `?` cannot live there). trait IntoStoreResult { fn store(self) -> RecordStoreResult; } -impl IntoStoreResult for Result { +impl IntoStoreResult for Result { fn store(self) -> RecordStoreResult { self.map_err(|e| RecordStoreError(render_pg_error(&e))) } @@ -76,7 +84,7 @@ impl IntoStoreResult for Result { /// The server's `detail` field is deliberately NOT included: on a unique violation Postgres puts /// the offending ROW VALUES in it, and this string reaches logs. The SQLSTATE, the primary message /// and the constraint name identify the failure without echoing data. -fn render_pg_error(e: &postgres::Error) -> String { +fn render_pg_error(e: &pgwire::Error) -> String { match e.as_db_error() { Some(db) => { let mut out = format!("{}: {}", db.code().code(), db.message()); @@ -93,8 +101,8 @@ fn render_pg_error(e: &postgres::Error) -> String { /// as an unversioned (version 0) database. Every other error class (connection, timeout, permission) /// is transient/fatal and must never be read as "fresh DB": treating a connection or permission /// failure as version 0 would drop and recreate a populated database. -fn is_undefined_table(e: &postgres::Error) -> bool { - e.code() == Some(&postgres::error::SqlState::UNDEFINED_TABLE) +fn is_undefined_table(e: &pgwire::Error) -> bool { + e.code().map(pgwire::SqlState::code) == Some(pgwire::SqlState::UNDEFINED_TABLE) } /// Extract the PASSWORD from a Postgres DSN. Supports both the URL form @@ -537,12 +545,36 @@ ALTER TABLE keys ADD COLUMN IF NOT EXISTS allowed_scopes_by_kind TEXT; ALTER TABLE usage_metering ADD COLUMN IF NOT EXISTS priced_from_ms BIGINT NOT NULL DEFAULT 0; "; -/// Postgres `Store` backend (durable, shared across a cluster). A single mutex-guarded connection — -/// governance is off the request hot path, so serializing access is fine. +/// Postgres `Store` backend (durable, shared across a cluster). The instance holds the parsed +/// connection settings, never a connection: every op is ONE connection over the host's connector +/// (the store SDK's `wire`), opened, authenticated and closed inside the op, so no socket of the +/// store's own exists and no op blocks a thread (busbar THE DESIGN, the connections section and +/// the plugin ABI). The schema is ensured once, by `open`'s connect step. pub struct PostgresStore { - client: Mutex, + shared: std::sync::Arc, +} + +/// What every op of one instance shares. +pub(crate) struct Shared { + /// The connection settings. + pub(crate) config: pgwire::Config, + /// The DSN's password, scrubbed from every connect-error text. + pub(crate) secret: Option, /// When this instance last swept `store_ops` past its retention (`v3`). - ops_swept_at: AtomicU64, + pub(crate) ops_swept_at: AtomicU64, +} + +/// ONE OP'S CONNECTION: the 1.5.5 bodies run on it, as they ran on the mutex-guarded client. +pub(crate) struct Session { + client: Client, + shared: std::sync::Arc, +} + +impl std::ops::Deref for Session { + type Target = Shared; + fn deref(&self) -> &Shared { + &self.shared + } } /// Clamp a `u64` into `i64` for a BIGINT column (a value above `i64::MAX` pins to `i64::MAX`, never @@ -579,59 +611,89 @@ fn now_secs() -> u64 { } impl PostgresStore { - /// Connect to Postgres with the given libpq connection string / URL and ensure the schema. TLS - /// is not wired in this build (`NoTls`); front the database with a TLS-terminating proxy or a - /// local socket. - pub fn connect(conn_str: &str) -> RecordStoreResult { + /// The store on the connection string `conn_str`: parsed here, connected per op. A string + /// the driver refuses is refused in its words, scrubbed of the DSN password. + /// + /// # Errors + /// The connection string does not parse. + pub fn new(conn_str: &str) -> RecordStoreResult { let secret = dsn_password(conn_str); - // `render_pg_error`, not `e.to_string()`. A server-side refusal (bad database name, failed - // authentication, an unavailable extension) is a `db_error` whose Display is the literal - // two words "db error" — so the ONE error an operator hits before anything else works - // rendered as the least actionable string in the crate, while every query error had already - // been fixed to carry its SQLSTATE and message. Still scrubbed of the DSN password. - let client = Client::connect(conn_str, NoTls) - .map_err(|e| RecordStoreError(scrub(render_pg_error(&e), secret.as_deref())))?; - let store = Self { - client: Mutex::new(client), - ops_swept_at: AtomicU64::new(0), - }; - store.migrate()?; - Ok(store) + let config = pgwire::Config::parse(conn_str) + .map_err(|e| RecordStoreError(scrub(e.to_string(), secret.as_deref())))?; + Ok(Self { + shared: std::sync::Arc::new(Shared { + config, + secret, + ops_swept_at: AtomicU64::new(0), + }), + }) + } + + pub(crate) fn shared(&self) -> std::sync::Arc { + self.shared.clone() + } +} + +impl Session { + /// Open one op's connection over `wire`: connect through the host's connector, secure it when + /// the settings ask, authenticate. A failure is the driver's words (`error connecting to + /// server: ...`, or the server's SQLSTATE and message), scrubbed of the DSN password. + pub(crate) async fn open( + wire: busbar_contract::abi::sdk::store::wire::Wire, + shared: std::sync::Arc, + ) -> Result { + match Client::connect(wire, &shared.config).await { + Ok(client) => Ok(Self { client, shared }), + Err(e) => Err(scrub(render_pg_error(&e), shared.secret.as_deref())), + } + } + + /// Say goodbye to the server; the op's connection closes when the op answers. + pub(crate) async fn close(mut self) { + self.client.terminate().await; + } + + fn lock(&mut self) -> &mut Client { + &mut self.client } - fn lock(&self) -> std::sync::MutexGuard<'_, Client> { - self.client.lock().unwrap_or_else(|p| p.into_inner()) + pub(crate) fn lock_client(&mut self) -> &mut Client { + &mut self.client } /// Open a transaction that gives every statement inside it ONE consistent snapshot, taken at the /// transaction's first statement — REPEATABLE READ, not the default READ COMMITTED (which gives /// each statement its own fresh snapshot, a torn-read hazard for any multi-statement read like /// `get_usage` or the hydration delta queries). - pub(crate) fn snapshot_consistent_tx<'a>( + pub(crate) async fn snapshot_consistent_tx<'a>( client: &'a mut Client, - ) -> RecordStoreResult> { + ) -> RecordStoreResult> { client .build_transaction() - .isolation_level(postgres::IsolationLevel::RepeatableRead) + .isolation_level(pgwire::IsolationLevel::RepeatableRead) .start() + .await .store() } const MIGRATE_LOCK_KEY: i64 = 0x6275_7362_6172_5f70; // ASCII "busbar_p" - fn migrate(&self) -> RecordStoreResult<()> { - let mut client = self.lock(); + pub(crate) async fn migrate(&mut self) -> RecordStoreResult<()> { + let client = self.lock(); client .batch_execute(&format!( "SELECT pg_advisory_lock({})", Self::MIGRATE_LOCK_KEY )) + .await .store()?; - let result = Self::migrate_locked(&mut client); - let unlocked = client.batch_execute(&format!( - "SELECT pg_advisory_unlock({})", - Self::MIGRATE_LOCK_KEY - )); + let result = Self::migrate_locked(client).await; + let unlocked = client + .batch_execute(&format!( + "SELECT pg_advisory_unlock({})", + Self::MIGRATE_LOCK_KEY + )) + .await; // A migration failure is the more important thing to report; don't mask it with an unlock // failure. But if the migration itself SUCCEEDED and the unlock did not, that must not be // swallowed: an un-released session-held advisory lock can hang a sibling node's connect() @@ -646,17 +708,20 @@ impl PostgresStore { } } - fn migrate_locked(client: &mut Client) -> RecordStoreResult<()> { + async fn migrate_locked(client: &mut Client) -> RecordStoreResult<()> { client .batch_execute("CREATE TABLE IF NOT EXISTS busbar_schema (version BIGINT PRIMARY KEY)") + .await .store()?; - let version: i64 = - match client.query_opt("SELECT COALESCE(MAX(version), 0) FROM busbar_schema", &[]) { - Ok(Some(r)) => r.get(0), - Ok(None) => 0, - Err(e) if is_undefined_table(&e) => 0, - Err(e) => return Err(RecordStoreError(e.to_string())), - }; + let version: i64 = match client + .query_opt("SELECT COALESCE(MAX(version), 0) FROM busbar_schema", &[]) + .await + { + Ok(Some(r)) => r.get(0), + Ok(None) => 0, + Err(e) if is_undefined_table(&e) => 0, + Err(e) => return Err(RecordStoreError(render_pg_error(&e))), + }; // ALREADY CURRENT: run no DDL at all. Every node runs `migrate()` on every connect, and // `SCHEMA`'s `CREATE INDEX IF NOT EXISTS` takes a SHARE lock on its table before it // discovers the index exists — first on `keys`, then on `credentials`. A `delete_key` on @@ -667,7 +732,7 @@ impl PostgresStore { if version >= SCHEMA_VERSION { return Ok(()); } - let mut tx = client.transaction().store()?; + let mut tx = client.transaction().await.store()?; if version < 5 { let legacy: bool = tx .query_one( @@ -676,6 +741,7 @@ impl PostgresStore { OR to_regclass('aws_credentials') IS NOT NULL", &[], ) + .await .store()? .get(0); if legacy { @@ -692,10 +758,11 @@ impl PostgresStore { DROP TABLE IF EXISTS denylist; DROP TABLE IF EXISTS store_revision;", ) + .await .store()?; } } - tx.batch_execute(SCHEMA).store()?; + tx.batch_execute(SCHEMA).await.store()?; // v6 one-time backfill — see SCHEMA_VERSION's doc comment for why this is safe as a // gated-once value backfill and would NOT be safe as a repeated/per-boot heuristic. Only // fires when crossing INTO v6 (a store already at v6+ never re-runs this). @@ -705,6 +772,7 @@ impl PostgresStore { WHERE billable_requests = 0 AND requests > 0", &[], ) + .await .store()?; } // v10 — see SCHEMA_VERSION. `SCHEMA`'s `CREATE TABLE IF NOT EXISTS` never alters a table @@ -719,7 +787,7 @@ impl PostgresStore { // also touches). The whole migration is ONE transaction with the version stamp, so a crash // part-way leaves `version < 10` and the next connect re-runs it from the start. if version < 10 { - tx.batch_execute(MIGRATE_V10_COLUMNS).store()?; + tx.batch_execute(MIGRATE_V10_COLUMNS).await.store()?; // The one v10 step that is not a pure addition: `priced_from_ms` joins the metering // primary key. Every existing row carries `priced_from_ms = 0` (the column default), // so the old key `(key_id, bucket, model, provider)` was already unique and the widened @@ -729,14 +797,16 @@ impl PostgresStore { ALTER TABLE usage_metering ADD CONSTRAINT usage_metering_pkey PRIMARY KEY (key_id, bucket, model, provider, priced_from_ms);", ) + .await .store()?; } tx.execute( "INSERT INTO busbar_schema (version) VALUES ($1) ON CONFLICT (version) DO NOTHING", &[&SCHEMA_VERSION], ) + .await .store()?; - tx.commit().store()?; + tx.commit().await.store()?; Ok(()) } @@ -746,11 +816,12 @@ impl PostgresStore { /// and credentials do): calling it first fixes a single lock-acquisition order /// (`store_revision` row, then whatever else the transaction touches) across every mutating /// method, which is what makes cross-method deadlock structurally impossible. - fn next_revision(tx: &mut Transaction<'_>) -> RecordStoreResult { + async fn next_revision(tx: &mut Transaction<'_>) -> RecordStoreResult { tx.query_one( "UPDATE store_revision SET revision = revision + 1 WHERE only_row RETURNING revision", &[], ) + .await .store()? .try_get(0) .store() @@ -895,16 +966,16 @@ fn row_to_cred_meta(r: &Row) -> CredentialMeta { } } -impl RecordStore for PostgresStore { - fn put_key(&self, key: &VirtualKey) -> RecordStoreResult<()> { +impl Session { + pub(crate) async fn put_key(&mut self, key: &VirtualKey) -> RecordStoreResult<()> { let (pools, by_kind) = scopes_to_storage(&key.allowed_scopes); let labels = labels_to_storage(&key.labels); let created = clamp(key.created_at); let expires = key.expires_at.map(clamp); let deleted = key.deleted_at.map(clamp); - let mut client = self.lock(); - let mut tx = client.transaction().store()?; - let rev = Self::next_revision(&mut tx)?; + let client = self.lock(); + let mut tx = client.transaction().await.store()?; + let rev = Self::next_revision(&mut tx).await?; // The `WHERE` on the conflict branch is the TOMBSTONE PRECONDITION (see `Store::put_key`): // a live-shaped write (`EXCLUDED.deleted_at IS NULL`) must not overwrite a tombstoned row, // which would reissue an id the contract says is never reissued and revive every token @@ -932,7 +1003,7 @@ impl RecordStore for PostgresStore { &key.group, &labels, &expires, &deleted, &rev, &key.idp_subject, &key.binding_mode, &key.minted_by, &by_kind, ], - ) + ).await .store()?; if changed == 0 { // The conflict branch matched a row but its WHERE rejected the write: the stored row is @@ -943,32 +1014,35 @@ impl RecordStore for PostgresStore { key.id ))); } - tx.commit().store()?; + tx.commit().await.store()?; Ok(()) } - fn get_key(&self, id: &str) -> RecordStoreResult> { + pub(crate) async fn get_key(&mut self, id: &str) -> RecordStoreResult> { let sql = format!("SELECT {KEY_COLUMNS} FROM keys WHERE id=$1"); - let row = self.lock().query_opt(&sql, &[&id]).store()?; + let row = self.lock().query_opt(&sql, &[&id]).await.store()?; Ok(row.map(|r| row_to_key(&r))) } - fn list_keys(&self) -> RecordStoreResult> { + pub(crate) async fn list_keys(&mut self) -> RecordStoreResult> { // Deliberately UNFILTERED (tombstoned rows included) -- see the trait doc: this serves both // the admin-listing caller (which filters deleted_at.is_none() itself) and list_keys_since's // default fallback, which needs tombstones visible to drive credential eviction downstream. let sql = format!("SELECT {KEY_COLUMNS} FROM keys ORDER BY created_at"); - let rows = self.lock().query(&sql, &[]).store()?; + let rows = self.lock().query(&sql, &[]).await.store()?; Ok(rows.iter().map(row_to_key).collect()) } - fn list_keys_since(&self, since: u64) -> RecordStoreResult> { + pub(crate) async fn list_keys_since( + &mut self, + since: u64, + ) -> RecordStoreResult> { let sql = format!("SELECT {KEY_COLUMNS} FROM keys WHERE revision > $1 ORDER BY revision"); - let rows = self.lock().query(&sql, &[&clamp(since)]).store()?; + let rows = self.lock().query(&sql, &[&clamp(since)]).await.store()?; Ok(rows.iter().map(row_to_key).collect()) } - fn delete_key(&self, id: &str) -> RecordStoreResult<()> { + pub(crate) async fn delete_key(&mut self, id: &str) -> RecordStoreResult<()> { // TOMBSTONE, not a hard delete: the `keys` row survives (billing/audit attribution keeps // resolving it forever) while every credential row for it is destroyed. Both happen in ONE // transaction stamped with the SAME revision, which is the load-bearing property for @@ -977,13 +1051,14 @@ impl RecordStore for PostgresStore { // reacts to "this key's revision-delta shows deleted_at newly set" by evicting all its // cached credentials is provably correct -- there is no window where the credential rows' // own (now-nonexistent) deltas would have been needed to convey the deletion. - let mut client = self.lock(); - let mut tx = client.transaction().store()?; + let client = self.lock(); + let mut tx = client.transaction().await.store()?; let already_deleted: Option = tx .query_opt( "SELECT deleted_at IS NOT NULL FROM keys WHERE id=$1", &[&id], ) + .await .store()? .map(|r| r.get(0)); match already_deleted { @@ -997,12 +1072,12 @@ impl RecordStore for PostgresStore { } Some(true) => { // Already tombstoned: no-op, not an error. - tx.commit().store()?; + tx.commit().await.store()?; return Ok(()); } Some(false) => {} } - let rev = Self::next_revision(&mut tx)?; + let rev = Self::next_revision(&mut tx).await?; // `deleted_at` is a WALL-CLOCK stamp and `revision` is the store-global counter. They are // both BIGINT, so binding one value to both columns compiles, round-trips and satisfies // every "is this key tombstoned" check -- while telling every operator, retention job and @@ -1011,6 +1086,7 @@ impl RecordStore for PostgresStore { // `delete_key_stamps_deleted_at_with_a_wall_clock_time_not_the_revision`. let now = clamp(now_secs()); tx.execute("DELETE FROM credentials WHERE key_id=$1", &[&id]) + .await .store()?; // `AND deleted_at IS NULL` re-states the guard the SELECT above already checked, IN the // UPDATE's own WHERE clause: under Postgres' READ COMMITTED semantics, an UPDATE takes a row @@ -1024,28 +1100,30 @@ impl RecordStore for PostgresStore { WHERE id=$1 AND deleted_at IS NULL", &[&id, &now, &rev], ) + .await .store()?; // A concurrent delete_key committed between our SELECT and this UPDATE: idempotent no-op, // same as the `Some(true)` branch above -- not an error. if changed == 0 { - tx.commit().store()?; + tx.commit().await.store()?; return Ok(()); } - tx.commit().store()?; + tx.commit().await.store()?; Ok(()) } - fn scrub_key(&self, id: &str) -> RecordStoreResult<()> { + pub(crate) async fn scrub_key(&mut self, id: &str) -> RecordStoreResult<()> { // PII-erasure only: null name/labels on an ALREADY-tombstoned key. Errors if unknown or // still live -- scrubbing a live key would be silent, un-auditable data loss on an active // principal (the trait doc's own guard: go through delete_key first). - let mut client = self.lock(); - let mut tx = client.transaction().store()?; + let client = self.lock(); + let mut tx = client.transaction().await.store()?; let deleted: Option = tx .query_opt( "SELECT deleted_at IS NOT NULL FROM keys WHERE id=$1", &[&id], ) + .await .store()? .map(|r| r.get(0)); match deleted { @@ -1057,7 +1135,7 @@ impl RecordStore for PostgresStore { } Some(true) => {} } - let rev = Self::next_revision(&mut tx)?; + let rev = Self::next_revision(&mut tx).await?; // `AND deleted_at IS NOT NULL` re-states the "must already be tombstoned" guard IN the // UPDATE's own WHERE clause, closing the same TOCTOU class as delete_key above: the SELECT // this function just ran is not atomic with this write, so without the re-check here a @@ -1071,6 +1149,7 @@ impl RecordStore for PostgresStore { WHERE id=$1 AND deleted_at IS NOT NULL", &[&id, &rev], ) + .await .store()?; if changed == 0 { return Err(RecordStoreError(format!( @@ -1078,20 +1157,25 @@ impl RecordStore for PostgresStore { call -- refusing to scrub a key that is live by the time the write landed" ))); } - tx.commit().store()?; + tx.commit().await.store()?; Ok(()) } - fn get_usage(&self, bucket_id: &str, window_start: u64) -> RecordStoreResult { + pub(crate) async fn get_usage( + &mut self, + bucket_id: &str, + window_start: u64, + ) -> RecordStoreResult { let ws = clamp(window_start); - let mut client = self.lock(); - let mut tx = Self::snapshot_consistent_tx(&mut client)?; + let client = self.lock(); + let mut tx = Self::snapshot_consistent_tx(client).await?; let (requests, billable_requests): (u64, u64) = tx .query_opt( "SELECT requests, billable_requests FROM usage_windows WHERE bucket_id=$1 AND window_start=$2", &[&bucket_id, &ws], ) + .await .store()? .map(|r| (read_u64(r.get::<_, i64>(0)), read_u64(r.get::<_, i64>(1)))) .unwrap_or((0, 0)); @@ -1101,6 +1185,7 @@ impl RecordStore for PostgresStore { FROM usage_ledger WHERE bucket_id=$1 AND window_start=$2 ORDER BY model", &[&bucket_id, &ws], ) + .await .store()?; let unit_rows = tx .query( @@ -1108,8 +1193,9 @@ impl RecordStore for PostgresStore { WHERE bucket_id=$1 AND window_start=$2 ORDER BY model, unit", &[&bucket_id, &ws], ) + .await .store()?; - tx.commit().store()?; + tx.commit().await.store()?; // One ModelTokens per model, in model order. The four reserved classes come off their // columns and every open class off `usage_ledger_units`, all into the one name-keyed map. // Zero counts are left out, so the map stays sparse the way busbar's own ledger keeps it. @@ -1140,8 +1226,8 @@ impl RecordStore for PostgresStore { }) } - fn put_usage( - &self, + pub(crate) async fn put_usage( + &mut self, bucket_id: &str, window_start: u64, ledger: &UsageLedger, @@ -1149,17 +1235,19 @@ impl RecordStore for PostgresStore { let ws = clamp(window_start); let rq = clamp(ledger.requests); let brq = clamp(ledger.billable_requests); - let mut client = self.lock(); - let mut tx = client.transaction().store()?; + let client = self.lock(); + let mut tx = client.transaction().await.store()?; tx.execute( "DELETE FROM usage_ledger WHERE bucket_id=$1 AND window_start=$2", &[&bucket_id, &ws], ) + .await .store()?; tx.execute( "DELETE FROM usage_ledger_units WHERE bucket_id=$1 AND window_start=$2", &[&bucket_id, &ws], ) + .await .store()?; tx.execute( "INSERT INTO usage_windows (bucket_id, window_start, requests, billable_requests) @@ -1169,6 +1257,7 @@ impl RecordStore for PostgresStore { billable_requests = EXCLUDED.billable_requests", &[&bucket_id, &ws, &rq, &brq], ) + .await .store()?; if !ledger.models.is_empty() { let rows: Vec<[i64; 4]> = ledger @@ -1202,7 +1291,7 @@ impl RecordStore for PostgresStore { params.push(v); } } - tx.execute(&sql, ¶ms).store()?; + tx.execute(&sql, ¶ms).await.store()?; // The OPEN unit classes: an absolute set too (the window's rows were cleared above). let opens: Vec<(&String, &String, i64)> = ledger @@ -1238,38 +1327,22 @@ impl RecordStore for PostgresStore { " ON CONFLICT (bucket_id, window_start, model, unit) DO UPDATE SET \ count = usage_ledger_units.count + EXCLUDED.count", ); - tx.execute(&sql, ¶ms).store()?; + tx.execute(&sql, ¶ms).await.store()?; } } - tx.commit().store()?; + tx.commit().await.store()?; Ok(()) } - fn add_usage( - &self, - bucket_id: &str, - window_start: u64, - delta: &UsageDelta, - ) -> RecordStoreResult<()> { - let mut client = self.lock(); - let mut tx = client.transaction().store()?; - Self::add_usage_in(&mut tx, bucket_id, window_start, delta)?; - tx.commit().store() - } - - fn add_metering(&self, d: &MeteringDelta) -> RecordStoreResult<()> { - let mut client = self.lock(); - let mut tx = client.transaction().store()?; - Self::add_metering_in(&mut tx, d)?; - tx.commit().store() - } - - fn list_metering(&self, bucket: u64) -> RecordStoreResult> { + pub(crate) async fn list_metering( + &mut self, + bucket: u64, + ) -> RecordStoreResult> { let b = clamp(bucket); - let mut client = self.lock(); + let client = self.lock(); // ONE snapshot for both reads, so a row's open classes are never read from a different // moment than its token columns. - let mut tx = Self::snapshot_consistent_tx(&mut client)?; + let mut tx = Self::snapshot_consistent_tx(client).await?; let rows = tx .query( "SELECT key_id, model, provider, @@ -1279,6 +1352,7 @@ impl RecordStore for PostgresStore { ORDER BY key_id, model, provider, priced_from_ms", &[&b], ) + .await .store()?; let unit_rows = tx .query( @@ -1286,8 +1360,9 @@ impl RecordStore for PostgresStore { FROM usage_metering_units WHERE bucket=$1", &[&b], ) + .await .store()?; - tx.commit().store()?; + tx.commit().await.store()?; type RowKey = (String, String, String, i64); let mut units: std::collections::HashMap> = std::collections::HashMap::new(); @@ -1324,25 +1399,28 @@ impl RecordStore for PostgresStore { .collect()) } - fn purge_windows_before(&self, before: u64) -> RecordStoreResult { + pub(crate) async fn purge_windows_before(&mut self, before: u64) -> RecordStoreResult { let b = clamp(before); - let mut client = self.lock(); - let mut tx = client.transaction().store()?; + let client = self.lock(); + let mut tx = client.transaction().await.store()?; let n1 = tx .execute("DELETE FROM usage_windows WHERE window_start < $1", &[&b]) + .await .store()?; tx.execute("DELETE FROM usage_ledger WHERE window_start < $1", &[&b]) + .await .store()?; tx.execute( "DELETE FROM usage_ledger_units WHERE window_start < $1", &[&b], ) + .await .store()?; - tx.commit().store()?; + tx.commit().await.store()?; Ok(n1) } - fn purge_metering_before(&self, bucket: &str) -> RecordStoreResult { + pub(crate) async fn purge_metering_before(&mut self, bucket: &str) -> RecordStoreResult { // The trait's purge_metering_before takes `bucket: &str` while list_metering/add_metering // use `bucket: u64` -- an inconsistency in the core trait itself, not introduced here. // usage_metering.bucket is genuinely BIGINT, so this parses the string form. @@ -1351,27 +1429,32 @@ impl RecordStore for PostgresStore { "purge_metering_before: invalid bucket {bucket:?}, expected an integer" )) })?; - let mut client = self.lock(); - let mut tx = client.transaction().store()?; + let client = self.lock(); + let mut tx = client.transaction().await.store()?; let n = tx .execute("DELETE FROM usage_metering WHERE bucket=$1", &[&b]) + .await .store()?; tx.execute("DELETE FROM usage_metering_units WHERE bucket=$1", &[&b]) + .await .store()?; - tx.commit().store()?; + tx.commit().await.store()?; Ok(n) } - fn put_credential(&self, secret: &CredentialSecret) -> RecordStoreResult<()> { - let mut client = self.lock(); - let mut tx = client.transaction().store()?; - Self::put_credential_tx(&mut tx, secret)?; - tx.commit().store()?; + pub(crate) async fn put_credential( + &mut self, + secret: &CredentialSecret, + ) -> RecordStoreResult<()> { + let client = self.lock(); + let mut tx = client.transaction().await.store()?; + Self::put_credential_tx(&mut tx, secret).await?; + tx.commit().await.store()?; Ok(()) } - fn put_key_with_credential( - &self, + pub(crate) async fn put_key_with_credential( + &mut self, key: &VirtualKey, secret: &CredentialSecret, ) -> RecordStoreResult<()> { @@ -1380,9 +1463,9 @@ impl RecordStore for PostgresStore { let labels = labels_to_storage(&key.labels); let created = clamp(key.created_at); let expires = key.expires_at.map(clamp); - let mut client = self.lock(); - let mut tx = client.transaction().store()?; - let rev = Self::next_revision(&mut tx)?; + let client = self.lock(); + let mut tx = client.transaction().await.store()?; + let rev = Self::next_revision(&mut tx).await?; // Same TOMBSTONE PRECONDITION as `put_key`, and this is the path where it MATTERS MOST: the // conflict branch used to set `deleted_at=NULL` outright, so re-minting over a tombstoned id // deliberately cleared the tombstone. That was written to avoid leaving a row both enabled @@ -1410,7 +1493,7 @@ impl RecordStore for PostgresStore { &key.group, &labels, &expires, &rev, &key.idp_subject, &key.binding_mode, &key.minted_by, &by_kind, ], - ) + ).await .store()?; if changed == 0 { return Err(RecordStoreError(format!( @@ -1419,26 +1502,33 @@ impl RecordStore for PostgresStore { key.id ))); } - Self::put_credential_tx(&mut tx, secret)?; - tx.commit().store()?; + Self::put_credential_tx(&mut tx, secret).await?; + tx.commit().await.store()?; Ok(()) } - fn list_credentials(&self, key_id: &str) -> RecordStoreResult> { + pub(crate) async fn list_credentials( + &mut self, + key_id: &str, + ) -> RecordStoreResult> { let sql = format!("SELECT {CRED_META_COLUMNS} FROM credentials WHERE key_id=$1"); - let rows = self.lock().query(&sql, &[&key_id]).store()?; + let rows = self.lock().query(&sql, &[&key_id]).await.store()?; Ok(rows.iter().map(row_to_cred_meta).collect()) } - fn lookup_credential_secret( - &self, + pub(crate) async fn lookup_credential_secret( + &mut self, kind: &str, public_id: &str, ) -> RecordStoreResult> { let sql = format!( "SELECT {CRED_META_COLUMNS},secret FROM credentials WHERE kind=$1 AND public_id=$2" ); - let row = self.lock().query_opt(&sql, &[&kind, &public_id]).store()?; + let row = self + .lock() + .query_opt(&sql, &[&kind, &public_id]) + .await + .store()?; Ok(row.map(|r| CredentialSecret { meta: row_to_cred_meta(&r), secret: r @@ -1447,14 +1537,19 @@ impl RecordStore for PostgresStore { })) } - fn revoke_credential(&self, id: &str, reason: &str) -> RecordStoreResult<()> { - let mut client = self.lock(); - let mut tx = client.transaction().store()?; + pub(crate) async fn revoke_credential( + &mut self, + id: &str, + reason: &str, + ) -> RecordStoreResult<()> { + let client = self.lock(); + let mut tx = client.transaction().await.store()?; let exists: bool = tx .query_one( "SELECT EXISTS(SELECT 1 FROM credentials WHERE id=$1)", &[&id], ) + .await .store()? .get(0); if !exists { @@ -1469,7 +1564,7 @@ impl RecordStore for PostgresStore { "revoke_credential: unknown credential id {id}; nothing was revoked" ))); } - let rev = Self::next_revision(&mut tx)?; + let rev = Self::next_revision(&mut tx).await?; let now = now_secs(); // `AND revoked_at IS NULL`: mirrors delete_key's `already_deleted` idempotency (see above) -- // a repeat revoke_credential call on an already-revoked row must be a true no-op, not bump @@ -1484,6 +1579,7 @@ impl RecordStore for PostgresStore { WHERE id=$1 AND revoked_at IS NULL", &[&id, &clamp(now), &reason, &rev], ) + .await .store()?; if changed == 0 { // Zero rows means one of two very different things, and the row count alone cannot tell @@ -1499,6 +1595,7 @@ impl RecordStore for PostgresStore { "SELECT EXISTS(SELECT 1 FROM credentials WHERE id=$1)", &[&id], ) + .await .store()? .get(0); if !still_exists { @@ -1512,15 +1609,18 @@ impl RecordStore for PostgresStore { // trait promises. The revision bump above goes unused, which its own doc allows (a // monotonic counter with gaps). } - tx.commit().store()?; + tx.commit().await.store()?; Ok(()) } - fn list_credentials_since(&self, since: u64) -> RecordStoreResult> { + pub(crate) async fn list_credentials_since( + &mut self, + since: u64, + ) -> RecordStoreResult> { let sql = format!( "SELECT {CRED_META_COLUMNS},secret FROM credentials WHERE revision > $1 ORDER BY revision" ); - let rows = self.lock().query(&sql, &[&clamp(since)]).store()?; + let rows = self.lock().query(&sql, &[&clamp(since)]).await.store()?; Ok(rows .iter() .map(|r| CredentialSecret { @@ -1532,45 +1632,28 @@ impl RecordStore for PostgresStore { .collect()) } - fn append_audit(&self, entry: &AuditRecord) -> RecordStoreResult<()> { - // The loop covers the one case the share lock cannot: the conflicting row disappearing - // BEFORE the read takes its lock. Then the seq is genuinely free again and the next - // iteration inserts. Bounded, and exhausting the bound is an error rather than a success, - // so no path here returns Ok without the record being stored. - const MAX_ATTEMPTS: u32 = 3; - for _ in 0..MAX_ATTEMPTS { - let mut client = self.lock(); - let mut tx = client.transaction().store()?; - if Self::append_audit_in(&mut tx, entry)? { - return tx.commit().store(); - } - } - Err(RecordStoreError(format!( - "append_audit: seq {} kept being freed between the insert and the read-back after \ - {MAX_ATTEMPTS} attempts; something is deleting audit rows concurrently and the record \ - was NOT stored", - entry.seq - ))) - } - - fn list_audit(&self) -> RecordStoreResult> { + pub(crate) async fn list_audit(&mut self) -> RecordStoreResult> { let sql = format!("SELECT {AUDIT_COLUMNS} FROM audit_log ORDER BY seq"); - let rows = self.lock().query(&sql, &[]).store()?; + let rows = self.lock().query(&sql, &[]).await.store()?; Ok(rows.iter().map(row_to_audit).collect()) } - fn list_audit_tail(&self, limit: u64) -> RecordStoreResult> { + pub(crate) async fn list_audit_tail( + &mut self, + limit: u64, + ) -> RecordStoreResult> { let sql = format!("SELECT {AUDIT_COLUMNS} FROM audit_log ORDER BY seq DESC LIMIT $1"); let rows = self .lock() .query(&sql, &[&i64::try_from(limit).unwrap_or(i64::MAX)]) + .await .store()?; let mut out: Vec = rows.iter().map(row_to_audit).collect(); out.reverse(); Ok(out) } - fn add_denylist(&self, sub: &str, reason: &str) -> RecordStoreResult<()> { + pub(crate) async fn add_denylist(&mut self, sub: &str, reason: &str) -> RecordStoreResult<()> { // `created_at` is a clock read, not the literal 0 it used to be. Same shape as the // `deleted_at` defect: the row lands, every "is this subject denied" check keeps working, // and only a question about WHEN it was denied reads back the epoch. store-sqlite writes @@ -1581,12 +1664,17 @@ impl RecordStore for PostgresStore { ON CONFLICT (sub) DO UPDATE SET reason = EXCLUDED.reason", &[&sub, &reason, &clamp(now_secs())], ) + .await .store()?; Ok(()) } - fn list_denylist(&self) -> RecordStoreResult> { - let rows = self.lock().query("SELECT sub FROM denylist", &[]).store()?; + pub(crate) async fn list_denylist(&mut self) -> RecordStoreResult> { + let rows = self + .lock() + .query("SELECT sub FROM denylist", &[]) + .await + .store()?; Ok(rows.iter().map(|r| r.get(0)).collect()) } @@ -1596,7 +1684,10 @@ impl RecordStore for PostgresStore { // in `plane_records` keyed `(kind, id)`; an APPENDED one in `plane_chain` keyed // `(kind, parent, seq)`. Identity, ordering and retention read only the typed sidecar columns. - fn upsert_plane_record(&self, record: PlaneRecordRef<'_>) -> RecordStoreResult<()> { + pub(crate) async fn upsert_plane_record( + &mut self, + record: PlaneRecordRef<'_>, + ) -> RecordStoreResult<()> { // This store binds owned rows: the one copy of the borrowed view happens here. let record = &record.to_record(); // REFUSED rather than clamped: `clamp` pins a value above i64::MAX, so the row read back @@ -1623,11 +1714,16 @@ impl RecordStore for PostgresStore { &record.body, ], ) + .await .store()?; Ok(()) } - fn get_plane_record(&self, kind: &str, id: &str) -> RecordStoreResult>> { + pub(crate) async fn get_plane_record( + &mut self, + kind: &str, + id: &str, + ) -> RecordStoreResult>> { // No principal filter, deliberately: caller scoping is ENGINE-side, because an // authorization check living in the backend is one an unauthorized reader bypasses by // configuring a different backend. @@ -1637,50 +1733,32 @@ impl RecordStore for PostgresStore { "SELECT body FROM plane_records WHERE kind=$1 AND id=$2", &[&kind, &id], ) + .await .store()?; Ok(row.map(|r| r.get(0))) } - fn append_plane_record(&self, record: PlaneRecordRef<'_>) -> RecordStoreResult<()> { - // This store binds owned rows: the one copy of the borrowed view happens here. - let record = &record.to_record(); - // The bounded loop covers the conflicting row vanishing before the share lock lands (the - // position is then free, so insert). No path returns Ok without the record being stored. - const MAX_ATTEMPTS: u32 = 3; - for _ in 0..MAX_ATTEMPTS { - let mut client = self.lock(); - let mut tx = client.transaction().store()?; - if Self::append_plane_record_in(&mut tx, record)? { - return tx.commit().store(); - } - } - Err(RecordStoreError(format!( - "append_plane_record: kind '{}' seq {} kept being freed between the insert and the \ - read-back after {MAX_ATTEMPTS} attempts; something is deleting chain rows \ - concurrently and the record was NOT stored", - record.kind, record.seq - ))) - } - - fn list_plane_records( - &self, + pub(crate) async fn list_plane_records( + &mut self, kind: &str, selector: &PlaneSelector<'_>, ) -> RecordStoreResult>> { - let mut client = self.lock(); + let client = self.lock(); // One snapshot across both tables. - let mut tx = Self::snapshot_consistent_tx(&mut client)?; + let mut tx = Self::snapshot_consistent_tx(client).await?; let (records, chain) = match selector { PlaneSelector::All => ( tx.query( "SELECT body FROM plane_records WHERE kind=$1 ORDER BY seq, id", &[&kind], ) + .await .store()?, tx.query( "SELECT body FROM plane_chain WHERE kind=$1 ORDER BY parent, seq", &[&kind], ) + .await .store()?, ), // Oldest-first by seq — the order the engine's chain verifier reads a parent's chain. @@ -1690,17 +1768,17 @@ impl RecordStore for PostgresStore { tx.query( "SELECT body FROM plane_records WHERE kind=$1 AND parent=$2 ORDER BY seq, id", &[&kind, &p], - ) + ).await .store()?, tx.query( "SELECT body FROM plane_chain WHERE kind=$1 AND parent=$2 ORDER BY seq", &[&kind, &p], - ) + ).await .store()?, ) } }; - tx.commit().store()?; + tx.commit().await.store()?; Ok(records .iter() .chain(chain.iter()) @@ -1708,7 +1786,10 @@ impl RecordStore for PostgresStore { .collect()) } - fn list_plane_record_parents(&self, kind: &str) -> RecordStoreResult> { + pub(crate) async fn list_plane_record_parents( + &mut self, + kind: &str, + ) -> RecordStoreResult> { // The boot enumeration a restart resumes chains from: every parent holding a record of // `kind`, each exactly once. let rows = self @@ -1720,18 +1801,23 @@ impl RecordStore for PostgresStore { ORDER BY 1", &[&kind], ) + .await .store()?; Ok(rows.iter().map(|r| r.get(0)).collect()) } - fn purge_plane_records_before(&self, kind: &str, before: u64) -> RecordStoreResult { + pub(crate) async fn purge_plane_records_before( + &mut self, + kind: &str, + before: u64, + ) -> RecordStoreResult { // STRICTLY older than the cutoff: a row exactly at `before` is kept. WHICH rows go is the // kind's own contract, read off the typed `disposition` column and never out of the body — // see TERMINAL_ONLY_RETENTION_KINDS. let cutoff = clamp(before); let terminal_only = TERMINAL_ONLY_RETENTION_KINDS.contains(&kind); - let mut client = self.lock(); - let mut tx = client.transaction().store()?; + let client = self.lock(); + let mut tx = client.transaction().await.store()?; let purged: Vec = tx .query( "DELETE FROM plane_records @@ -1739,6 +1825,7 @@ impl RecordStore for PostgresStore { RETURNING id", &[&kind, &cutoff, &terminal_only], ) + .await .store()? .iter() .map(|r| r.get(0)) @@ -1748,6 +1835,7 @@ impl RecordStore for PostgresStore { "DELETE FROM plane_chain WHERE kind=$1 AND ts < $2", &[&kind, &cutoff], ) + .await .store()?; // CASCADE, in the SAME transaction: a purged parent's child chain goes with it, and only // the chains under a record that actually went — so this can never be a second, wider @@ -1758,36 +1846,43 @@ impl RecordStore for PostgresStore { "DELETE FROM plane_chain WHERE kind=$1 AND parent = ANY($2)", &[child, &purged], ) + .await .store()?; } } - tx.commit().store()?; + tx.commit().await.store()?; // `execute`/`RETURNING` report the rows actually removed, so the count is one performed. Ok(purged.len() as u64 + chain) } - fn delete_plane_record(&self, kind: &str, id: &str) -> RecordStoreResult<()> { + pub(crate) async fn delete_plane_record( + &mut self, + kind: &str, + id: &str, + ) -> RecordStoreResult<()> { // Absent is a NO-OP, not an error: the engine clears on every observation that agrees with // the operator rather than tracking whether it had written one. A chain whose parent is // `id` goes too, so deleting a record cannot leave part of its chain behind. - let mut client = self.lock(); - let mut tx = client.transaction().store()?; + let client = self.lock(); + let mut tx = client.transaction().await.store()?; tx.execute( "DELETE FROM plane_records WHERE kind=$1 AND id=$2", &[&kind, &id], ) + .await .store()?; tx.execute( "DELETE FROM plane_chain WHERE kind=$1 AND parent=$2", &[&kind, &id], ) + .await .store()?; - tx.commit().store()?; + tx.commit().await.store()?; Ok(()) } - fn redeem_plane_token( - &self, + pub(crate) async fn redeem_plane_token( + &mut self, kind: &str, token: &str, expires_at: u64, @@ -1799,8 +1894,8 @@ impl RecordStore for PostgresStore { // every spent token. An error is refused by the engine, so it is the direction to fail in. let expires = as_storable_i64("redeem_plane_token", "expires_at", expires_at)?; let cutoff = as_storable_i64("redeem_plane_token", "now", now)?; - let mut client = self.lock(); - let mut tx = client.transaction().store()?; + let client = self.lock(); + let mut tx = client.transaction().await.store()?; // The eviction sweep the redemption carries, bounding the ledger by one validity window: an // entry recording a token that can no longer be presented protects nothing. STRICTLY // less-than (an entry expiring exactly at `now` is kept), and BEFORE the insert, so it can @@ -1809,6 +1904,7 @@ impl RecordStore for PostgresStore { "DELETE FROM plane_tokens WHERE kind=$1 AND expires_at < $2", &[&kind, &cutoff], ) + .await .store()?; // THE TEST AND SET, as ONE statement: `execute` returns the rows this INSERT wrote, so 1 // means THIS call recorded the redemption and 0 means it was already there. A read then a @@ -1820,13 +1916,14 @@ impl RecordStore for PostgresStore { ON CONFLICT (kind, token) DO NOTHING", &[&kind, &token, &expires], ) + .await .store()?; - tx.commit().store()?; + tx.commit().await.store()?; Ok(inserted == 1) } - fn plane_token_live( - &self, + pub(crate) async fn plane_token_live( + &mut self, kind: &str, token: &str, expires_at: u64, @@ -1845,6 +1942,7 @@ impl RecordStore for PostgresStore { "SELECT disposition FROM plane_records WHERE kind=$1 AND id=$2", &[&kind, &token], ) + .await .store()?; Ok(row.is_some_and(|r| r.get::<_, &str>(0) == "active")) } @@ -1885,19 +1983,19 @@ fn row_to_audit(r: &Row) -> AuditRecord { } } -impl PostgresStore { +impl Session { /// Shared body of `put_credential`/`put_key_with_credential`: upsert on `(key_id, kind, slot)`. /// Minting into an OCCUPIED LIVE slot (revoked_at IS NULL) MUST fail rather than silently /// destroy a working credential mid-overlap-window -- the `WHERE credentials.revoked_at IS NOT /// NULL` guard on the `DO UPDATE` makes that structural: the upsert simply does not apply if /// the existing row is live, and the subsequent `changed` check turns that into a real error /// instead of a silent no-op. - fn put_credential_tx( + async fn put_credential_tx( tx: &mut Transaction<'_>, secret: &CredentialSecret, ) -> RecordStoreResult<()> { let m = &secret.meta; - let rev = Self::next_revision(tx)?; + let rev = Self::next_revision(tx).await?; // The owning key must EXIST and be LIVE (`put_credential`'s precondition, pinned by the // conformance suite). `delete_key` cascades a key's credentials away precisely so the // secret material stops resolving; accepting a mint afterwards puts it back under a key an @@ -1911,6 +2009,7 @@ impl PostgresStore { "SELECT deleted_at IS NULL FROM keys WHERE id=$1", &[&m.key_id], ) + .await .store()? .map(|r| r.get(0)); match owner_live { @@ -1961,7 +2060,7 @@ impl PostgresStore { &expires, &rev, ], - ) + ).await .store()?; if changed == 0 { // Either the slot is occupied by a LIVE credential (the WHERE guard blocked it), or this @@ -1971,7 +2070,7 @@ impl PostgresStore { .query_one( "SELECT EXISTS(SELECT 1 FROM credentials WHERE key_id=$1 AND kind=$2 AND slot=$3)", &[&m.key_id, &m.kind, &(m.slot as i16)], - ) + ).await .store()? .get(0); if exists { @@ -1992,10 +2091,10 @@ impl PostgresStore { } /// The writes the store v3 `op_id` slots run inside their dedupe transaction ([`v3`]). -impl PostgresStore { +impl Session { /// `add_usage` inside the caller's transaction (the store v3 `op_id` writes and batches run /// it with their dedupe record, in one transaction). - pub(crate) fn add_usage_in( + pub(crate) async fn add_usage_in( tx: &mut Transaction<'_>, bucket_id: &str, window_start: u64, @@ -2010,6 +2109,7 @@ impl PostgresStore { billable_requests = GREATEST(0, usage_windows.billable_requests + $4::bigint)", &[&bucket_id, &ws, &delta.requests, &delta.billable_requests], ) + .await .store()?; if !delta.models.is_empty() { // TWO statements, both batched over every model, and the split is what makes a @@ -2068,8 +2168,8 @@ impl PostgresStore { ") AS v(model, di, do_, dcr, dcw) \ WHERE u.bucket_id = $1 AND u.window_start = $2 AND u.model = v.model", ); - tx.execute(&ensure, &ensure_params).store()?; - tx.execute(&update, &update_params).store()?; + tx.execute(&ensure, &ensure_params).await.store()?; + tx.execute(&update, &update_params).await.store()?; // The OPEN unit classes, the same ensure-then-signed-update shape. let opens: Vec<(&String, &String, i64)> = delta @@ -2123,15 +2223,15 @@ impl PostgresStore { WHERE u.bucket_id = $1 AND u.window_start = $2 \ AND u.model = v.model AND u.unit = v.unit", ); - tx.execute(&ensure, &ensure_params).store()?; - tx.execute(&update, &update_params).store()?; + tx.execute(&ensure, &ensure_params).await.store()?; + tx.execute(&update, &update_params).await.store()?; } } Ok(()) } /// `add_metering` inside the caller's transaction. - pub(crate) fn add_metering_in( + pub(crate) async fn add_metering_in( tx: &mut Transaction<'_>, d: &MeteringDelta, ) -> RecordStoreResult<()> { @@ -2164,7 +2264,7 @@ impl PostgresStore { &d.key_id, &bucket, &d.model, &d.provider, &ti, &to, &tcr, &tcw, &requests, &brequests, &d.key_group_at_use, &d.pricing_version, &priced_from, ], - ) + ).await .store()?; // Every ledgered class the token columns do not hold, additive like them, in the SAME // transaction so a metering row never shows its tokens without its other classes. @@ -2198,7 +2298,7 @@ impl PostgresStore { " ON CONFLICT (key_id, bucket, model, provider, priced_from_ms, unit) DO UPDATE SET \ count = usage_metering_units.count + EXCLUDED.count", ); - tx.execute(&sql, ¶ms).store()?; + tx.execute(&sql, ¶ms).await.store()?; } Ok(()) } @@ -2206,7 +2306,7 @@ impl PostgresStore { /// `append_audit` inside the caller's transaction: `Ok(true)` once the record is stored (or an /// identical one already was), `Ok(false)` when the conflicting row vanished before the share /// lock held it (the seq is free again: retry in a fresh transaction), `Err` on a fork. - pub(crate) fn append_audit_in( + pub(crate) async fn append_audit_in( tx: &mut Transaction<'_>, entry: &AuditRecord, ) -> RecordStoreResult { @@ -2266,6 +2366,7 @@ impl PostgresStore { &entry.hash, ], ) + .await .store()?; if inserted == 1 { return Ok(true); @@ -2273,6 +2374,7 @@ impl PostgresStore { let sql = format!("SELECT {AUDIT_COLUMNS} FROM audit_log WHERE seq=$1 FOR SHARE"); let stored = tx .query_opt(&sql, &[&seq]) + .await .store()? .map(|r| row_to_audit(&r)); match stored { @@ -2290,7 +2392,7 @@ impl PostgresStore { /// `append_plane_record` inside the caller's transaction: `Ok(true)` once the record is stored /// (or an identical one already was), `Ok(false)` when the conflicting row vanished before the /// share lock held it (retry in a fresh transaction), `Err` on a fork. - pub(crate) fn append_plane_record_in( + pub(crate) async fn append_plane_record_in( tx: &mut Transaction<'_>, record: &PlaneRecord, ) -> RecordStoreResult { @@ -2325,6 +2427,7 @@ impl PostgresStore { &record.body, ], ) + .await .store()?; if inserted == 1 { return Ok(true); @@ -2335,6 +2438,7 @@ impl PostgresStore { WHERE kind=$1 AND parent=$2 AND seq=$3 FOR SHARE", &[&record.kind, &parent, &seq], ) + .await .store()?; match stored { Some(r) => { diff --git a/store-postgres/src/pgwire.rs b/store-postgres/src/pgwire.rs new file mode 100644 index 0000000..7b1b31b --- /dev/null +++ b/store-postgres/src/pgwire.rs @@ -0,0 +1,1128 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE POSTGRES FRONTEND PROTOCOL OVER THE HOST'S CONNECTOR: a small async client whose every byte +//! goes through the op's one connection ([`Wire`], the store SDK's `wire`), so this store opens no +//! socket of its own (busbar THE DESIGN, the connections section). The codec is +//! `postgres-protocol` (the messages, SCRAM-SHA-256 and md5) and the value encodings are +//! `postgres-types` (the same binary `ToSql`/`FromSql` the `postgres` crate uses), so a parameter +//! or a column reads exactly as it did over the 1.5.5 driver. +//! +//! The surface mirrors the `postgres` crate's (`Client`, `Transaction`, `Row`, `Error`) with every +//! call `async`, so the store's SQL bodies are the 1.5.5 bodies with `.await`. A query is two round +//! trips (Parse/Describe/Sync for the parameter and column types, then Bind/Execute/Sync), every +//! parameter and column in the binary format. A `Transaction` dropped without `commit` is rolled +//! back before the connection's next statement, as the driver's was. + +use std::fmt; +use std::ops::Range; +use std::sync::Arc; + +use bytes::BytesMut; +use fallible_iterator::FallibleIterator; +use postgres_protocol::authentication::sasl::{self, ChannelBinding, ScramSha256}; +use postgres_protocol::message::backend::{DataRowBody, ErrorResponseBody, Message}; +use postgres_protocol::message::frontend; +use postgres_types::{FromSql, Kind, ToSql, Type}; + +use busbar_contract::abi::sdk::store::wire::Wire; + +// ── configuration ─────────────────────────────────────────────────────────────────────────── + +/// Whether the connection is secured (libpq's `sslmode`). `disable`, `allow` and `prefer` connect +/// in plaintext, as the 1.5.5 build (`NoTls`) did for every mode it accepted; `require`, +/// `verify-ca` and `verify-full` ask the server for TLS and secure the connection through the host +/// (its trust anchors), refusing a server that declines. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SslMode { + /// Plaintext. + Plain, + /// TLS, or no connection. + Require, +} + +/// A parsed connection string (the URL or the libpq keyword form). +#[derive(Clone, PartialEq, Eq)] +pub struct Config { + pub host: String, + pub port: u16, + pub user: String, + pub password: Option, + pub dbname: Option, + pub application_name: Option, + pub options: Option, + pub ssl: SslMode, +} + +impl fmt::Debug for Config { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Config") + .field("host", &self.host) + .field("port", &self.port) + .field("user", &self.user) + .field("dbname", &self.dbname) + .finish_non_exhaustive() + } +} + +fn config_err(what: impl Into) -> Error { + Error::new(Kind_::Config(what.into())) +} + +impl Config { + /// Parse a libpq connection string: `postgres[ql]://user:pass@host:port/db?key=value&...` or + /// `host=... port=... user=... password=... dbname=... sslmode=...`. + /// + /// # Errors + /// A malformed string, an unknown option, a Unix-socket host (this store reaches its server + /// over the host's connector, which dials TCP) or no user. + pub fn parse(s: &str) -> Result { + let mut pairs: Vec<(String, String)> = Vec::new(); + if let Some(rest) = s + .strip_prefix("postgresql://") + .or_else(|| s.strip_prefix("postgres://")) + { + let (main, query) = match rest.split_once('?') { + Some((m, q)) => (m, Some(q)), + None => (rest, None), + }; + let (authority, path) = match main.split_once('/') { + Some((a, p)) => (a, Some(p)), + None => (main, None), + }; + let (userinfo, hostport) = match authority.rsplit_once('@') { + Some((u, h)) => (Some(u), h), + None => (None, authority), + }; + if let Some(u) = userinfo { + match u.split_once(':') { + Some((user, pass)) => { + pairs.push(("user".into(), decode(user)?)); + pairs.push(("password".into(), decode(pass)?)); + } + None => pairs.push(("user".into(), decode(u)?)), + } + } + if !hostport.is_empty() { + if hostport.contains(',') { + return Err(config_err("multiple hosts are not supported")); + } + let (host, port) = if let Some(v6) = hostport.strip_prefix('[') { + match v6.split_once(']') { + Some((h, rest)) => (h.to_string(), rest.strip_prefix(':')), + None => return Err(config_err("unterminated IPv6 host")), + } + } else { + match hostport.rsplit_once(':') { + Some((h, p)) => (h.to_string(), Some(p)), + None => (hostport.to_string(), None), + } + }; + if !host.is_empty() { + pairs.push(("host".into(), decode(&host)?)); + } + if let Some(p) = port.filter(|p| !p.is_empty()) { + pairs.push(("port".into(), p.to_string())); + } + } + if let Some(db) = path.filter(|p| !p.is_empty()) { + pairs.push(("dbname".into(), decode(db)?)); + } + if let Some(q) = query { + for kv in q.split('&').filter(|kv| !kv.is_empty()) { + let (k, v) = kv + .split_once('=') + .ok_or_else(|| config_err(format!("invalid query parameter `{kv}`")))?; + pairs.push((decode(k)?, decode(v)?)); + } + } + } else { + pairs = keywords(s)?; + } + let mut c = Config { + host: String::new(), + port: 5432, + user: String::new(), + password: None, + dbname: None, + application_name: None, + options: None, + ssl: SslMode::Plain, + }; + for (k, v) in pairs { + match k.as_str() { + "host" | "hostaddr" => c.host = v, + "port" => { + c.port = v + .parse() + .map_err(|_| config_err("invalid value for option `port`"))?; + } + "user" => c.user = v, + "password" => c.password = Some(v), + "dbname" => c.dbname = Some(v), + "application_name" => c.application_name = Some(v), + "options" => c.options = Some(v), + "sslmode" => { + c.ssl = match v.as_str() { + "disable" | "allow" | "prefer" => SslMode::Plain, + "require" | "verify-ca" | "verify-full" => SslMode::Require, + _ => { + return Err(config_err("invalid value for option `sslmode`")); + } + } + } + // Accepted and served by the host instead: its deadlines bound every connect, and + // its connector keeps the connection. + "connect_timeout" + | "keepalives" + | "keepalives_idle" + | "target_session_attrs" + | "channel_binding" + | "load_balance_hosts" + | "tcp_user_timeout" + | "keepalives_interval" + | "keepalives_retries" + | "sslrootcert" + | "sslnegotiation" => {} + _ => return Err(config_err(format!("unknown option `{k}`"))), + } + } + if c.host.is_empty() { + c.host = "localhost".into(); + } + if c.host.starts_with('/') { + return Err(config_err( + "a Unix-socket host is not supported: the store reaches its server over TCP", + )); + } + if c.user.is_empty() { + return Err(config_err("user missing")); + } + Ok(c) + } + + /// The `host:port` the store's need dials. + #[must_use] + pub fn target(&self) -> String { + if self.host.contains(':') { + format!("[{}]:{}", self.host, self.port) + } else { + format!("{}:{}", self.host, self.port) + } + } +} + +fn decode(s: &str) -> Result { + let b = s.as_bytes(); + let mut out = Vec::with_capacity(b.len()); + let mut i = 0; + while i < b.len() { + if b[i] == b'%' { + let hex = |c: u8| (c as char).to_digit(16); + match ( + b.get(i + 1).and_then(|c| hex(*c)), + b.get(i + 2).and_then(|c| hex(*c)), + ) { + (Some(h), Some(l)) => { + out.push((h * 16 + l) as u8); + i += 3; + continue; + } + _ => return Err(config_err("invalid percent encoding")), + } + } + out.push(b[i]); + i += 1; + } + String::from_utf8(out).map_err(|_| config_err("invalid percent encoding")) +} + +/// The libpq keyword form: `key=value` pairs, whitespace around `=` allowed, single-quoted values +/// with `\'` and `\\` escapes. +fn keywords(s: &str) -> Result, Error> { + let mut out = Vec::new(); + let mut it = s.chars().peekable(); + loop { + while it.peek().is_some_and(|c| c.is_whitespace()) { + it.next(); + } + if it.peek().is_none() { + return Ok(out); + } + let mut key = String::new(); + while let Some(&c) = it.peek() { + if c == '=' || c.is_whitespace() { + break; + } + key.push(c); + it.next(); + } + while it.peek().is_some_and(|c| c.is_whitespace()) { + it.next(); + } + if it.next() != Some('=') { + return Err(config_err(format!("unexpected EOF after `{key}`"))); + } + while it.peek().is_some_and(|c| c.is_whitespace()) { + it.next(); + } + let mut value = String::new(); + if it.peek() == Some(&'\'') { + it.next(); + loop { + match it.next() { + None => { + return Err(config_err("unterminated quoted connection parameter value")) + } + Some('\'') => break, + Some('\\') => match it.next() { + Some(c) => value.push(c), + None => { + return Err(config_err( + "unterminated quoted connection parameter value", + )) + } + }, + Some(c) => value.push(c), + } + } + } else { + while let Some(&c) = it.peek() { + if c.is_whitespace() { + break; + } + if c == '\\' { + it.next(); + if let Some(n) = it.next() { + value.push(n); + } + continue; + } + value.push(c); + it.next(); + } + } + out.push((key, value)); + } +} + +// ── errors ────────────────────────────────────────────────────────────────────────────────── + +/// A SQLSTATE. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SqlState(String); + +impl SqlState { + /// `42P01`, `undefined_table`. + pub const UNDEFINED_TABLE: &'static str = "42P01"; + + /// The five-character code. + #[must_use] + pub fn code(&self) -> &str { + &self.0 + } +} + +/// A server-side failure (an `ErrorResponse`). +#[derive(Debug, Clone)] +pub struct DbError { + severity: String, + code: SqlState, + message: String, + constraint: Option, +} + +impl DbError { + fn parse(body: &ErrorResponseBody) -> Self { + let mut e = DbError { + severity: String::new(), + code: SqlState(String::new()), + message: String::new(), + constraint: None, + }; + let mut fields = body.fields(); + while let Ok(Some(f)) = fields.next() { + let v = String::from_utf8_lossy(f.value_bytes()).into_owned(); + match f.type_() { + b'S' => e.severity = v, + b'C' => e.code = SqlState(v), + b'M' => e.message = v, + b'n' => e.constraint = Some(v), + _ => {} + } + } + e + } + + /// The SQLSTATE. + #[must_use] + pub fn code(&self) -> &SqlState { + &self.code + } + + /// The primary message. + #[must_use] + pub fn message(&self) -> &str { + &self.message + } + + /// The violated constraint, if one. + #[must_use] + pub fn constraint(&self) -> Option<&str> { + self.constraint.as_deref() + } +} + +#[derive(Debug)] +enum Kind_ { + Db(DbError), + Connect(String), + Io(String), + Closed, + Parse(String), + Tls(String), + Authentication(String), + Config(String), + ToSql(usize, String), + FromSql(usize, String), + Column(String), + RowCount, + Parameters(usize, usize), +} + +/// A driver error, in the `postgres` crate's words. +#[derive(Debug)] +pub struct Error(Box); + +impl Error { + fn new(k: Kind_) -> Self { + Self(Box::new(k)) + } + + /// The server's failure, when it is one. + #[must_use] + pub fn as_db_error(&self) -> Option<&DbError> { + match &*self.0 { + Kind_::Db(d) => Some(d), + _ => None, + } + } + + /// The server's SQLSTATE, when it is a server failure. + #[must_use] + pub fn code(&self) -> Option<&SqlState> { + self.as_db_error().map(DbError::code) + } + + /// A server failure with SQLSTATE `code` and `message`, as an `ErrorResponse` decodes. + #[cfg(test)] + #[must_use] + pub fn server(code: &str, message: &str) -> Self { + Self::new(Kind_::Db(DbError { + severity: "ERROR".to_string(), + code: SqlState(code.to_string()), + message: message.to_string(), + constraint: None, + })) + } + + /// A connect failure (the host's connector could not reach the server). + #[must_use] + pub fn connect(cause: impl fmt::Display) -> Self { + Self::new(Kind_::Connect(cause.to_string())) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &*self.0 { + Kind_::Db(d) => write!(f, "db error: {}: {}", d.severity, d.message), + Kind_::Connect(c) => write!(f, "error connecting to server: {c}"), + Kind_::Io(c) => write!(f, "error communicating with the server: {c}"), + Kind_::Closed => f.write_str("connection closed"), + Kind_::Parse(c) => write!(f, "error parsing response from server: {c}"), + Kind_::Tls(c) => write!(f, "error performing TLS handshake: {c}"), + Kind_::Authentication(c) => write!(f, "authentication error: {c}"), + Kind_::Config(c) => write!(f, "invalid configuration: {c}"), + Kind_::ToSql(i, c) => write!(f, "error serializing parameter {i}: {c}"), + Kind_::FromSql(i, c) => write!(f, "error deserializing column {i}: {c}"), + Kind_::Column(c) => write!(f, "invalid column `{c}`"), + Kind_::RowCount => f.write_str("query returned an unexpected number of rows"), + Kind_::Parameters(want, got) => { + write!(f, "expected {want} parameters but got {got}") + } + } + } +} + +impl std::error::Error for Error {} + +fn parse_err(e: impl fmt::Display) -> Error { + Error::new(Kind_::Parse(e.to_string())) +} + +// ── rows ──────────────────────────────────────────────────────────────────────────────────── + +/// A result column. +#[derive(Debug, Clone)] +pub struct Column { + name: String, + type_: Type, +} + +/// What names a column of a [`Row`]: its index or its name. +pub trait RowIndex: fmt::Display { + #[doc(hidden)] + fn index(&self, columns: &[Column]) -> Option; +} + +impl RowIndex for usize { + fn index(&self, columns: &[Column]) -> Option { + (*self < columns.len()).then_some(*self) + } +} + +impl RowIndex for &str { + fn index(&self, columns: &[Column]) -> Option { + columns.iter().position(|c| c.name == *self) + } +} + +/// One result row. +#[derive(Debug)] +pub struct Row { + columns: Arc<[Column]>, + body: DataRowBody, + ranges: Vec>>, +} + +impl Row { + /// Column `idx` as `T`. + /// + /// # Panics + /// An unknown column, or a value that does not convert (the `postgres` crate's `get`). + pub fn get<'a, I: RowIndex, T: FromSql<'a>>(&'a self, idx: I) -> T { + match self.try_get(idx) { + Ok(v) => v, + Err(e) => panic!("error retrieving column: {e}"), + } + } + + /// Column `idx` as `T`. + /// + /// # Errors + /// An unknown column, a type `T` does not accept, or a value that does not convert. + pub fn try_get<'a, I: RowIndex, T: FromSql<'a>>(&'a self, idx: I) -> Result { + let Some(i) = idx.index(&self.columns) else { + return Err(Error::new(Kind_::Column(idx.to_string()))); + }; + let ty = &self.columns[i].type_; + if !T::accepts(ty) { + return Err(Error::new(Kind_::FromSql( + i, + format!( + "cannot convert between the Rust type `{}` and the Postgres type `{}`", + std::any::type_name::(), + ty + ), + ))); + } + let raw = self.ranges[i].clone().map(|r| &self.body.buffer()[r]); + T::from_sql_nullable(ty, raw).map_err(|e| Error::new(Kind_::FromSql(i, e.to_string()))) + } +} + +// ── the client ────────────────────────────────────────────────────────────────────────────── + +/// One connection to the server, over the op's wire. +pub struct Client { + wire: Wire, + buf: BytesMut, + /// A `Transaction` was dropped uncommitted: roll it back before the next statement. + rollback_pending: bool, +} + +impl fmt::Debug for Client { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Client").finish_non_exhaustive() + } +} + +/// A parameter. +pub type Param<'a> = &'a (dyn ToSql + Sync); + +impl Client { + /// Connect over `wire` (its need 0) to the server `cfg` names, secure the connection when + /// `cfg` asks, and authenticate. + /// + /// # Errors + /// The connector's failure (as a connect error), a TLS refusal, or the server's refusal. + pub async fn connect(wire: Wire, cfg: &Config) -> Result { + wire.connect(0, Some(&cfg.target())) + .await + .map_err(Error::connect)?; + let mut c = Client { + wire, + buf: BytesMut::new(), + rollback_pending: false, + }; + if cfg.ssl == SslMode::Require { + let mut out = BytesMut::new(); + frontend::ssl_request(&mut out); + c.send(&out).await?; + let answer = c.read_byte().await?; + if answer != b'S' { + return Err(Error::new(Kind_::Tls( + "server does not support TLS".to_string(), + ))); + } + c.wire + .upgrade_secure(Some(&cfg.host)) + .await + .map_err(|e| Error::new(Kind_::Tls(e.to_string())))?; + } + c.startup(cfg).await?; + Ok(c) + } + + async fn startup(&mut self, cfg: &Config) -> Result<(), Error> { + let mut params: Vec<(&str, &str)> = vec![("client_encoding", "UTF8"), ("user", &cfg.user)]; + if let Some(db) = &cfg.dbname { + params.push(("database", db)); + } + if let Some(o) = &cfg.options { + params.push(("options", o)); + } + if let Some(a) = &cfg.application_name { + params.push(("application_name", a)); + } + let mut out = BytesMut::new(); + frontend::startup_message(params.iter().copied(), &mut out).map_err(parse_err)?; + self.send(&out).await?; + let password = || { + cfg.password + .as_deref() + .map(str::as_bytes) + .ok_or_else(|| Error::new(Kind_::Config("password missing".to_string()))) + }; + loop { + match self.read().await? { + Message::AuthenticationOk => break, + Message::AuthenticationCleartextPassword => { + let mut out = BytesMut::new(); + frontend::password_message(password()?, &mut out).map_err(parse_err)?; + self.send(&out).await?; + } + Message::AuthenticationMd5Password(b) => { + let hash = postgres_protocol::authentication::md5_hash( + cfg.user.as_bytes(), + password()?, + b.salt(), + ); + let mut out = BytesMut::new(); + frontend::password_message(hash.as_bytes(), &mut out).map_err(parse_err)?; + self.send(&out).await?; + } + Message::AuthenticationSasl(b) => { + let mut scram = false; + let mut mechs = b.mechanisms(); + while let Some(m) = mechs.next().map_err(parse_err)? { + scram |= m == sasl::SCRAM_SHA_256; + } + if !scram { + return Err(Error::new(Kind_::Authentication( + "unsupported SASL mechanism".to_string(), + ))); + } + let mut s = ScramSha256::new(password()?, ChannelBinding::unsupported()); + let mut out = BytesMut::new(); + frontend::sasl_initial_response(sasl::SCRAM_SHA_256, s.message(), &mut out) + .map_err(parse_err)?; + self.send(&out).await?; + let Message::AuthenticationSaslContinue(cont) = self.read_auth().await? else { + return Err(parse_err("unexpected message during SASL")); + }; + s.update(cont.data()) + .map_err(|e| Error::new(Kind_::Authentication(e.to_string())))?; + let mut out = BytesMut::new(); + frontend::sasl_response(s.message(), &mut out).map_err(parse_err)?; + self.send(&out).await?; + let Message::AuthenticationSaslFinal(fin) = self.read_auth().await? else { + return Err(parse_err("unexpected message during SASL")); + }; + s.finish(fin.data()) + .map_err(|e| Error::new(Kind_::Authentication(e.to_string())))?; + } + Message::ErrorResponse(e) => return Err(Error::new(Kind_::Db(DbError::parse(&e)))), + Message::NoticeResponse(_) => {} + _ => { + return Err(Error::new(Kind_::Authentication( + "unsupported authentication method".to_string(), + ))) + } + } + } + loop { + match self.read().await? { + Message::ReadyForQuery(_) => return Ok(()), + Message::ErrorResponse(e) => return Err(Error::new(Kind_::Db(DbError::parse(&e)))), + _ => {} + } + } + } + + /// An authentication message, a server failure answered as one. + async fn read_auth(&mut self) -> Result { + match self.read().await? { + Message::ErrorResponse(e) => Err(Error::new(Kind_::Db(DbError::parse(&e)))), + m => Ok(m), + } + } + + /// Say goodbye (`Terminate`), best effort: the op's connection closes when it answers. + pub async fn terminate(&mut self) { + let mut out = BytesMut::new(); + frontend::terminate(&mut out); + let _ = self.wire.write_all(&out).await; + } + + async fn send(&mut self, bytes: &[u8]) -> Result<(), Error> { + self.wire + .write_all(bytes) + .await + .map_err(|e| Error::new(Kind_::Io(e.to_string()))) + } + + async fn more(&mut self) -> Result<(), Error> { + let n = self + .wire + .fill() + .await + .map_err(|e| Error::new(Kind_::Io(e.to_string())))?; + if n == 0 { + return Err(Error::new(Kind_::Closed)); + } + let buf = &mut self.buf; + self.wire.input(|i| { + buf.extend_from_slice(i); + i.clear(); + }); + Ok(()) + } + + async fn read_byte(&mut self) -> Result { + while self.buf.is_empty() { + self.more().await?; + } + let b = self.buf[0]; + let _ = self.buf.split_to(1); + Ok(b) + } + + /// The next message the server sent; asynchronous notices and parameter changes skipped. + async fn read(&mut self) -> Result { + loop { + match Message::parse(&mut self.buf).map_err(parse_err)? { + Some(Message::ParameterStatus(_) | Message::NoticeResponse(_)) => {} + Some(Message::NotificationResponse(_)) => {} + Some(m) => return Ok(m), + None => self.more().await?, + } + } + } + + /// Read to `ReadyForQuery`, after a failure. + async fn drain(&mut self) -> Result<(), Error> { + loop { + if let Message::ReadyForQuery(_) = self.read().await? { + return Ok(()); + } + } + } + + /// Roll back a transaction dropped uncommitted. + async fn settle(&mut self) -> Result<(), Error> { + if std::mem::take(&mut self.rollback_pending) { + self.simple("ROLLBACK").await?; + } + Ok(()) + } + + async fn simple(&mut self, sql: &str) -> Result<(), Error> { + let mut out = BytesMut::new(); + frontend::query(sql, &mut out).map_err(parse_err)?; + self.send(&out).await?; + let mut failed = None; + loop { + match self.read().await? { + Message::ReadyForQuery(_) => break, + Message::ErrorResponse(e) if failed.is_none() => { + failed = Some(Error::new(Kind_::Db(DbError::parse(&e)))); + } + _ => {} + } + } + failed.map_or(Ok(()), Err) + } + + /// Run `sql` (one or more statements, no parameters) through the simple query protocol. + /// + /// # Errors + /// The first statement's failure. + pub async fn batch_execute(&mut self, sql: &str) -> Result<(), Error> { + self.settle().await?; + self.simple(sql).await + } + + /// Run one statement with `params`: its rows and the rows it affected. + async fn run(&mut self, sql: &str, params: &[Param<'_>]) -> Result<(Vec, u64), Error> { + self.settle().await?; + // Round trip 1: the parameter and column types. + let mut out = BytesMut::new(); + frontend::parse("", sql, std::iter::empty(), &mut out).map_err(parse_err)?; + frontend::describe(b'S', "", &mut out).map_err(parse_err)?; + frontend::sync(&mut out); + self.send(&out).await?; + let mut types: Vec = Vec::new(); + let mut columns: Vec = Vec::new(); + loop { + match self.read().await? { + Message::ParseComplete | Message::NoData => {} + Message::ParameterDescription(p) => { + let mut it = p.parameters(); + while let Some(oid) = it.next().map_err(parse_err)? { + types.push(type_of(oid)); + } + } + Message::RowDescription(r) => { + let mut it = r.fields(); + while let Some(f) = it.next().map_err(parse_err)? { + columns.push(Column { + name: f.name().to_string(), + type_: type_of(f.type_oid()), + }); + } + } + Message::ErrorResponse(e) => { + let err = Error::new(Kind_::Db(DbError::parse(&e))); + self.drain().await?; + return Err(err); + } + Message::ReadyForQuery(_) => break, + _ => return Err(parse_err("unexpected message")), + } + } + if types.len() != params.len() { + return Err(Error::new(Kind_::Parameters(types.len(), params.len()))); + } + // Round trip 2: bind, execute. + let mut out = BytesMut::new(); + let mut failed: Option<(usize, String)> = None; + let mut index = 0; + let bound = frontend::bind( + "", + "", + std::iter::repeat_n(1_i16, params.len()), + params.iter().zip(types.iter()), + |(p, ty), buf| { + let i = index; + index += 1; + match p.to_sql_checked(ty, buf) { + Ok(postgres_types::IsNull::No) => Ok(postgres_protocol::IsNull::No), + Ok(postgres_types::IsNull::Yes) => Ok(postgres_protocol::IsNull::Yes), + Err(e) => { + failed = Some((i, e.to_string())); + Err(e) + } + } + }, + std::iter::once(1_i16), + &mut out, + ); + if let Err(e) = bound { + return Err(match failed { + Some((i, t)) => Error::new(Kind_::ToSql(i, t)), + None => match e { + frontend::BindError::Conversion(e) => { + Error::new(Kind_::ToSql(0, e.to_string())) + } + frontend::BindError::Serialization(e) => parse_err(e), + }, + }); + } + frontend::execute("", 0, &mut out).map_err(parse_err)?; + frontend::sync(&mut out); + self.send(&out).await?; + let columns: Arc<[Column]> = columns.into(); + let mut rows = Vec::new(); + let mut affected = 0; + let mut failed = None; + loop { + match self.read().await? { + Message::BindComplete | Message::EmptyQueryResponse => {} + Message::DataRow(body) => { + if failed.is_none() { + let mut ranges = Vec::with_capacity(columns.len()); + let mut it = body.ranges(); + while let Some(r) = it.next().map_err(parse_err)? { + ranges.push(r); + } + rows.push(Row { + columns: columns.clone(), + body, + ranges, + }); + } + } + Message::CommandComplete(c) => { + let tag = c.tag().map_err(parse_err)?; + affected = tag + .rsplit(' ') + .next() + .and_then(|n| n.parse().ok()) + .unwrap_or(0); + } + Message::ErrorResponse(e) if failed.is_none() => { + failed = Some(Error::new(Kind_::Db(DbError::parse(&e)))); + } + Message::ReadyForQuery(_) => break, + _ => {} + } + } + match failed { + Some(e) => Err(e), + None => Ok((rows, affected)), + } + } + + /// The rows `sql` answers. + /// + /// # Errors + /// The statement's failure. + pub async fn query(&mut self, sql: &str, params: &[Param<'_>]) -> Result, Error> { + self.run(sql, params).await.map(|(r, _)| r) + } + + /// How many rows `sql` affected. + /// + /// # Errors + /// The statement's failure. + pub async fn execute(&mut self, sql: &str, params: &[Param<'_>]) -> Result { + self.run(sql, params).await.map(|(_, n)| n) + } + + /// The one row `sql` answers. + /// + /// # Errors + /// The statement's failure, or not exactly one row. + pub async fn query_one(&mut self, sql: &str, params: &[Param<'_>]) -> Result { + let mut rows = self.query(sql, params).await?; + if rows.len() != 1 { + return Err(Error::new(Kind_::RowCount)); + } + Ok(rows.remove(0)) + } + + /// The row `sql` answers, if one. + /// + /// # Errors + /// The statement's failure, or more than one row. + pub async fn query_opt( + &mut self, + sql: &str, + params: &[Param<'_>], + ) -> Result, Error> { + let mut rows = self.query(sql, params).await?; + match rows.len() { + 0 => Ok(None), + 1 => Ok(Some(rows.remove(0))), + _ => Err(Error::new(Kind_::RowCount)), + } + } + + /// Begin a transaction (the server's default isolation, READ COMMITTED). + /// + /// # Errors + /// The `BEGIN`'s failure. + pub async fn transaction(&mut self) -> Result, Error> { + self.batch_execute("BEGIN").await?; + Ok(Transaction { + client: self, + done: false, + }) + } + + /// A transaction with options. + pub fn build_transaction(&mut self) -> TransactionBuilder<'_> { + TransactionBuilder { + client: self, + isolation: None, + } + } +} + +fn type_of(oid: u32) -> Type { + Type::from_oid(oid) + .unwrap_or_else(|| Type::new(format!("{oid}"), oid, Kind::Simple, String::new())) +} + +/// A transaction's isolation level. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IsolationLevel { + RepeatableRead, +} + +/// The statement that begins a transaction at `isolation` (`None` = the server's default). +#[must_use] +pub fn begin_sql(isolation: Option) -> &'static str { + match isolation { + None => "BEGIN", + Some(IsolationLevel::RepeatableRead) => "BEGIN ISOLATION LEVEL REPEATABLE READ", + } +} + +/// [`Client::build_transaction`]. +pub struct TransactionBuilder<'a> { + client: &'a mut Client, + isolation: Option, +} + +impl<'a> TransactionBuilder<'a> { + /// The isolation level. + #[must_use] + pub fn isolation_level(mut self, level: IsolationLevel) -> Self { + self.isolation = Some(level); + self + } + + /// Begin it. + /// + /// # Errors + /// The `BEGIN`'s failure. + pub async fn start(self) -> Result, Error> { + self.client.batch_execute(begin_sql(self.isolation)).await?; + Ok(Transaction { + client: self.client, + done: false, + }) + } +} + +/// A transaction; rolled back unless committed. +pub struct Transaction<'a> { + client: &'a mut Client, + done: bool, +} + +impl Drop for Transaction<'_> { + fn drop(&mut self) { + if !self.done { + self.client.rollback_pending = true; + } + } +} + +impl Transaction<'_> { + /// Commit. + /// + /// # Errors + /// The `COMMIT`'s failure. + pub async fn commit(mut self) -> Result<(), Error> { + self.done = true; + self.client.batch_execute("COMMIT").await + } + + /// [`Client::batch_execute`] inside the transaction. + /// + /// # Errors + /// As there. + pub async fn batch_execute(&mut self, sql: &str) -> Result<(), Error> { + self.client.batch_execute(sql).await + } + + /// [`Client::query`] inside the transaction. + /// + /// # Errors + /// As there. + pub async fn query(&mut self, sql: &str, params: &[Param<'_>]) -> Result, Error> { + self.client.query(sql, params).await + } + + /// [`Client::execute`] inside the transaction. + /// + /// # Errors + /// As there. + pub async fn execute(&mut self, sql: &str, params: &[Param<'_>]) -> Result { + self.client.execute(sql, params).await + } + + /// [`Client::query_one`] inside the transaction. + /// + /// # Errors + /// As there. + pub async fn query_one(&mut self, sql: &str, params: &[Param<'_>]) -> Result { + self.client.query_one(sql, params).await + } + + /// [`Client::query_opt`] inside the transaction. + /// + /// # Errors + /// As there. + pub async fn query_opt( + &mut self, + sql: &str, + params: &[Param<'_>], + ) -> Result, Error> { + self.client.query_opt(sql, params).await + } +} + +#[cfg(test)] +mod tests { + use super::{begin_sql, Config, IsolationLevel, SslMode}; + + #[test] + fn a_url_parses_into_its_parts() { + let c = Config::parse( + "postgres://u%40x:p%3Ass@db.internal:6543/busbar?sslmode=require&application_name=bb", + ) + .unwrap(); + assert_eq!(c.user, "u@x"); + assert_eq!(c.password.as_deref(), Some("p:ss")); + assert_eq!(c.target(), "db.internal:6543"); + assert_eq!(c.dbname.as_deref(), Some("busbar")); + assert_eq!(c.ssl, SslMode::Require); + assert_eq!(c.application_name.as_deref(), Some("bb")); + let c = Config::parse("postgresql://u@[::1]/d").unwrap(); + assert_eq!(c.target(), "[::1]:5432"); + assert_eq!(c.password, None); + } + + #[test] + fn the_keyword_form_parses_with_spaces_and_quotes() { + let c = + Config::parse("host = db port=5433 user=u password='se cret' dbname=x sslmode=prefer") + .unwrap(); + assert_eq!(c.target(), "db:5433"); + assert_eq!(c.password.as_deref(), Some("se cret")); + assert_eq!(c.ssl, SslMode::Plain); + } + + #[test] + fn what_the_host_cannot_dial_is_refused() { + assert!(Config::parse("host=/var/run/postgresql user=u").is_err()); + assert!(Config::parse("host=db").is_err(), "no user"); + assert!(Config::parse("host=db user=u bogus=1").is_err()); + assert!(Config::parse("host=db user=u sslmode=sometimes").is_err()); + assert!(Config::parse("postgres://u@a,b/d").is_err()); + } + + #[test] + fn a_snapshot_transaction_begins_repeatable_read() { + assert_eq!( + begin_sql(Some(IsolationLevel::RepeatableRead)), + "BEGIN ISOLATION LEVEL REPEATABLE READ" + ); + assert_eq!(begin_sql(None), "BEGIN"); + } +} diff --git a/store-postgres/src/tests.rs b/store-postgres/src/tests.rs index ea13749..944a146 100644 --- a/store-postgres/src/tests.rs +++ b/store-postgres/src/tests.rs @@ -8,9 +8,12 @@ use super::*; use busbar_contract::records::{ CredentialMeta, CredentialSecret, ModelTokensDelta, PlaneDisposition, PlaneRecord, - PlaneSelector, SecretForm, + PlaneSelector, RecordStore, SecretForm, }; +mod harness; +pub(crate) use harness::TestStore; + /// One model's ledger row from the four RESERVED token classes (zero classes left out, the way the /// store reads them back). fn mt( @@ -169,23 +172,19 @@ fn connect_client_with_retry(url: &str) -> postgres::Client { panic!("connect (after retries): {:?}", last_err.unwrap()); } -/// `PostgresStore::connect` with bounded retry-with-backoff. Each connect opens a fresh connection -/// AND runs `migrate()`, so under the core gate's parallel-test load against a shared Postgres a -/// single connect can be TRANSIENTLY refused when the server is momentarily at its connection -/// ceiling (surfacing as `RecordStoreError("db error")` / too-many-clients) -- the exact class of flake -/// the gate hit on the isolation test's connect. Every live-DB test needs at least this one store -/// connection, so footprint reduction alone can't harden the primary connect; retrying ~10 times -/// over a couple of seconds absorbs the transient. Returns the same `RecordStoreResult` `connect` does, so -/// each caller keeps its own `.expect(...)` message. NOT used where a connect is EXPECTED to fail -/// (the permission test asserts `.is_err()` directly and must not spin on a genuine, persistent -/// error). -fn connect_store_with_retry(url: &str) -> RecordStoreResult { +/// The store under test, opened with bounded retry-with-backoff: through the REAL loader on a +/// dispatcher, its connections over the host's connection path (the loader's test `TcpConns`), so +/// every op goes through the door exactly as busbar runs it. Each open runs the connect step (a +/// connection and `migrate()`), so under parallel-test load against a shared Postgres a single +/// open can be TRANSIENTLY refused on connection pressure; retrying ~10 times over a couple of +/// seconds absorbs that. NOT used where an open is EXPECTED to fail. +fn connect_store_with_retry(url: &str) -> Result { let mut last_err = None; for attempt in 0..10u32 { if attempt > 0 { std::thread::sleep(std::time::Duration::from_millis(50 * attempt as u64)); } - match PostgresStore::connect(url) { + match TestStore::open(url) { Ok(store) => return Ok(store), Err(e) => last_err = Some(e), } @@ -198,7 +197,7 @@ fn connect_store_with_retry(url: &str) -> RecordStoreResult { /// re-running the suite (or running it twice in a row) never sees stale state /// from a prior run leaking into the CHECK constraints (e.g. re-minting into a row still marked /// `deleted_at` from a previous run's tombstone would violate `keys_tombstone_disabled`). -fn hard_reset(store: &PostgresStore, id: &str) { +fn hard_reset(store: &TestStore, id: &str) { let mut client = store.lock(); let _ = client.execute("DELETE FROM credentials WHERE key_id=$1", &[&id]); let _ = client.execute("DELETE FROM keys WHERE id=$1", &[&id]); @@ -790,24 +789,22 @@ fn concurrent_delete_of_the_same_key_is_safe_and_idempotent() { fn get_usage_transaction_is_actually_repeatable_read() { let Some(url) = live_url() else { return }; let store = connect_store_with_retry(&url).expect("connect"); - // Drive the isolation check through the store's OWN connection -- the exact same client - // get_usage uses -- rather than opening a second raw `postgres::Client`. That extra, uncounted - // connection was pure connection-footprint overhead here (this assertion never needed a - // *separate* connection, only a real one), and under the core gate's parallel-test load against - // a shared Postgres its `.connect()` transiently failed on connection pressure -- the observed - // flake panicked at this test's connect path (`connect: RecordStoreError("db error")`), never on the - // isolation assertion below. Reusing the store's client (extending what b2f3804 did for the - // sibling torn-read test) halves this test's connection count and removes the refuse-able - // connect, while testing the real helper against a real client just as faithfully. The scope - // guard releases the store lock before `get_usage` (which re-locks the same mutex) is called. + // The store's connections are its ops' own (one per op, over the host's connector), so the + // isolation check runs the helper's exact statement (`pgwire::begin_sql`, the + // `IsolationLevel::RepeatableRead` arm `snapshot_consistent_tx` uses) on an independent driver + // connection and asks the server what it opened. let level: String = { let mut client = store.lock(); - let mut tx = PostgresStore::snapshot_consistent_tx(&mut client).unwrap(); - let level: String = tx + client + .batch_execute(crate::pgwire::begin_sql(Some( + crate::pgwire::IsolationLevel::RepeatableRead, + ))) + .unwrap(); + let level: String = client .query_one("SHOW transaction_isolation", &[]) .unwrap() .get(0); - tx.commit().unwrap(); + client.batch_execute("COMMIT").unwrap(); level }; assert_eq!( @@ -861,7 +858,12 @@ fn get_usage_snapshot_does_not_observe_a_concurrent_add_usage_between_its_two_re // test's -- instead its connect is bounded-retried to absorb transient connection-pressure // refusals under the gate's parallel load. let mut client = connect_client_with_retry(&url); - let mut tx = PostgresStore::snapshot_consistent_tx(&mut client).unwrap(); + // `snapshot_consistent_tx`'s statement, on this independent connection. + let mut tx = client + .build_transaction() + .isolation_level(postgres::IsolationLevel::RepeatableRead) + .start() + .unwrap(); let _requests_row = tx .query_one( "SELECT requests, billable_requests FROM usage_windows WHERE bucket_id=$1 AND window_start=$2", @@ -984,30 +986,25 @@ fn percent_decode_edge_cases() { } /// `is_undefined_table` must discriminate the ONE SQLSTATE (`42P01`/undefined_table) it exists to -/// recognize from every other error class -- pinned against two REAL postgres errors (never a -/// hand-built one, since `postgres::Error` has no public constructor), so neither an inverted -/// comparison nor an unconditional true/false would pass. +/// recognize from every other error class, so neither an inverted comparison nor an unconditional +/// true/false passes. The errors are the store's own driver's server-failure shape (`pgwire`'s +/// `ErrorResponse` decode), carrying the codes a real server sends for a missing table and for a +/// syntax error; the permission test below proves the classification against a live server. #[test] fn is_undefined_table_matches_only_the_real_sqlstate() { - let Some(url) = live_url() else { return }; - let mut client = postgres::Client::connect(&url, postgres::NoTls).unwrap(); - - let missing = client - .query_opt( - "SELECT 1 FROM spg_this_table_definitely_does_not_exist_xyz", - &[], - ) - .unwrap_err(); + let missing = crate::pgwire::Error::server("42P01", "relation \"x\" does not exist"); assert!( is_undefined_table(&missing), - "a query against a genuinely missing table must be classified as undefined_table: {missing}" + "a missing table must be classified as undefined_table: {missing}" ); - - let syntax_err = client.query_opt("SELEC 1", &[]).unwrap_err(); + let syntax_err = crate::pgwire::Error::server("42601", "syntax error at or near \"SELEC\""); assert!( !is_undefined_table(&syntax_err), "a syntax error must NOT be misclassified as undefined_table: {syntax_err}" ); + assert!(!is_undefined_table(&crate::pgwire::Error::connect( + "refused" + ))); } /// `labels_to_storage`'s serialization -- not just the empty-map default -- must round-trip a @@ -1942,11 +1939,14 @@ fn migrate_propagates_a_non_undefined_table_error_and_never_silently_succeeds() "test setup sanity: the probe query must fail with insufficient_privilege, not \ undefined_table, for this test to mean anything: {probe_err}" ); - assert!(!is_undefined_table(&probe_err)); + assert_ne!( + probe_err.code(), + Some(&postgres::error::SqlState::UNDEFINED_TABLE) + ); } assert!( - PostgresStore::connect(&limited_url).is_err(), + TestStore::open(&limited_url).is_err(), "migrate must never silently succeed against a role that can't actually read its own \ bookkeeping table" ); @@ -1970,7 +1970,7 @@ mod store_conformance; mod conformance { use super::plane_records::lock_plane_purge; use super::store_conformance as conf; - use super::{clamp, connect_store_with_retry, live_url, PostgresStore}; + use super::{clamp, connect_store_with_retry, live_url, TestStore}; /// A per-process, PER-CHECK namespace. Short enough for every id column in the schema. /// @@ -1984,7 +1984,7 @@ mod conformance { /// Delete every row this suite is about to write, so a rerun (or a crashed prior run that left /// rows behind) starts from the same state as a first run. Plane rows are matched on the /// EXACT `{ns}_` prefix every plane fixture id carries (not `LIKE`, whose `_` is a wildcard). - fn reset(store: &PostgresStore, ns: &str, seq: u64) { + fn reset(store: &TestStore, ns: &str, seq: u64) { let mut client = store.lock(); for id in conf::key_ids(ns) { let _ = client.execute("DELETE FROM credentials WHERE key_id=$1", &[&id]); @@ -2010,7 +2010,7 @@ mod conformance { ); } - fn setup(check: &str, seq: u64) -> Option<(PostgresStore, String)> { + fn setup(check: &str, seq: u64) -> Option<(TestStore, String)> { let url = live_url()?; let store = connect_store_with_retry(&url).expect("connect"); let ns = ns(check); @@ -2023,7 +2023,7 @@ mod conformance { let Some((store, ns)) = setup("put", 0) else { return; }; - conf::assert_put_key_does_not_resurrect_a_tombstone(&store, &ns); + conf::assert_put_key_does_not_resurrect_a_tombstone(&*store, &ns); } #[test] @@ -2031,7 +2031,7 @@ mod conformance { let Some((store, ns)) = setup("del", 0) else { return; }; - conf::assert_delete_key_unknown_id_is_an_error(&store, &ns); + conf::assert_delete_key_unknown_id_is_an_error(&*store, &ns); } #[test] @@ -2039,7 +2039,7 @@ mod conformance { let Some((store, ns)) = setup("rev", 0) else { return; }; - conf::assert_revoke_credential_unknown_id_is_an_error(&store, &ns); + conf::assert_revoke_credential_unknown_id_is_an_error(&*store, &ns); } #[test] @@ -2047,7 +2047,7 @@ mod conformance { let Some((store, ns)) = setup("live", 0) else { return; }; - conf::assert_put_credential_requires_a_live_key(&store, &ns); + conf::assert_put_credential_requires_a_live_key(&*store, &ns); } #[test] @@ -2055,7 +2055,7 @@ mod conformance { let Some((store, ns)) = setup("atom", 0) else { return; }; - conf::assert_put_key_with_credential_is_atomic(&store, &ns); + conf::assert_put_key_with_credential_is_atomic(&*store, &ns); } #[test] @@ -2067,7 +2067,7 @@ mod conformance { return; }; let _audit_guard = super::lock_audit_table(); - conf::assert_append_audit_duplicate_seq(&store, seq); + conf::assert_append_audit_duplicate_seq(&*store, seq); } #[test] @@ -2075,7 +2075,7 @@ mod conformance { let Some((store, ns)) = setup("ptask", 0) else { return; }; - conf::assert_plane_task_upsert_get_list(&store, &ns); + conf::assert_plane_task_upsert_get_list(&*store, &ns); } #[test] @@ -2083,7 +2083,7 @@ mod conformance { let Some((store, ns)) = setup("pchain", 0) else { return; }; - conf::assert_plane_event_chain_is_ordered_by_seq(&store, &ns); + conf::assert_plane_event_chain_is_ordered_by_seq(&*store, &ns); } #[test] @@ -2091,7 +2091,7 @@ mod conformance { let Some((store, ns)) = setup("pprin", 0) else { return; }; - conf::assert_plane_call_parents_enumerated(&store, &ns); + conf::assert_plane_call_parents_enumerated(&*store, &ns); } #[test] @@ -2099,7 +2099,7 @@ mod conformance { let Some((store, ns)) = setup("pdem", 0) else { return; }; - conf::assert_plane_demotion_upsert_list_delete(&store, &ns); + conf::assert_plane_demotion_upsert_list_delete(&*store, &ns); } #[test] @@ -2110,7 +2110,7 @@ mod conformance { // This binary's own purge tests sweep the same kinds with a HIGHER cutoff, which would // reach this check's newer-than-cutoff survivor; they hold the same lock. let _guard = lock_plane_purge(); - conf::assert_plane_purge_honours_the_cutoff(&store, &ns); + conf::assert_plane_purge_honours_the_cutoff(&*store, &ns); } #[test] @@ -2119,7 +2119,7 @@ mod conformance { return; }; let _guard = lock_plane_purge(); - conf::assert_plane_purge_task_keeps_active_rows(&store, &ns); + conf::assert_plane_purge_task_keeps_active_rows(&*store, &ns); } #[test] @@ -2127,7 +2127,7 @@ mod conformance { let Some((store, ns)) = setup("ptok", 0) else { return; }; - conf::assert_plane_token_is_single_use(&store, &ns); + conf::assert_plane_token_is_single_use(&*store, &ns); } } diff --git a/store-postgres/src/tests/harness.rs b/store-postgres/src/tests/harness.rs new file mode 100644 index 0000000..cae1a83 --- /dev/null +++ b/store-postgres/src/tests/harness.rs @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE STORE UNDER TEST, AS BUSBAR RUNS IT: the store's door loaded through the real loader on a +//! dispatcher, every connection over the host's connection path (the loader's test connection +//! table, `tcp_conns::TcpConns`, plain TCP), opened with `open`'s connect step. [`TestStore`] +//! answers the 1.5.5 op set through the loader's synchronous bridge (`RecordStore`, by deref) and +//! the store v3 slots through `StoreCalls`, in the slots' own result types; [`TestStore::lock`] is +//! an INDEPENDENT `postgres` driver connection for the raw SQL a test sets up or verifies with. + +use std::future::Future; +use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; +use std::task::{Context, Poll, Wake, Waker}; + +use busbar_contract::abi::sdk::store::{ + Cap, CapsRefused, Cell, Grant, OpRefused, ReserveRefused, StoreSlots, +}; +use busbar_contract::abi::store::OpId; +use busbar_contract::kinds::{Head, RecordBytes}; +use busbar_contract::records::{AuditRecord, UsageDelta}; +use busbar_contract::store_calls::{StoreCalls, StoreFailure}; +use busbar_plugin_loader::dispatch::kinds::store::Store; +use busbar_plugin_loader::dispatch::{ + load_linked, Bind, DispatchConfig, Dispatcher, LinkedRow, NoSink, +}; +use busbar_plugin_loader::store_v3::LoadedStore; +use busbar_plugin_loader::tcp_conns::TcpConns; + +struct Unpark(std::thread::Thread); +impl Wake for Unpark { + fn wake(self: Arc) { + self.0.unpark(); + } +} + +/// Run `f` to completion on this thread. +pub(crate) fn block_on(f: F) -> F::Output { + let mut f = std::pin::pin!(f); + let waker = Waker::from(Arc::new(Unpark(std::thread::current()))); + let mut cx = Context::from_waker(&waker); + loop { + if let Poll::Ready(v) = f.as_mut().poll(&mut cx) { + return v; + } + std::thread::park(); + } +} + +/// A fresh `op_id` for every bridge write: a node half no other open (in this run or an earlier +/// one) used, the store dedupes `op_id`s DURABLY. +fn mint() -> OpId { + static NODE: std::sync::OnceLock = std::sync::OnceLock::new(); + static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + let node = *NODE.get_or_init(|| { + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + nanos ^ (u64::from(std::process::id()) << 40) + }); + OpId::from_parts( + node, + N.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1, + ) +} + +/// The store's linked door opened on `settings` over the loader, its needs on a [`TcpConns`]. +pub(crate) fn open_loaded(settings: &str) -> Result { + let d = Arc::new(Dispatcher::new(DispatchConfig::default())); + let conns: Arc = + Arc::new(TcpConns::new(d.conn_waker())); + let row = LinkedRow::of(crate::door).map_err(|e| e.to_string())?; + let p = load_linked::( + &row, + Bind { + instance: Arc::from("store-postgres-test"), + max_inflight_cap: 64, + sink: Arc::new(NoSink), + dispatcher: d.adopter(), + conns: Some(conns), + }, + ) + .map_err(|e| e.to_string())?; + LoadedStore::open(p, d, settings.as_bytes(), mint) +} + +/// THE STORE UNDER TEST. +pub(crate) struct TestStore { + store: LoadedStore, + url: String, + raw: Mutex>, +} + +impl std::ops::Deref for TestStore { + type Target = LoadedStore; + fn deref(&self) -> &LoadedStore { + &self.store + } +} + +fn op_refused(f: StoreFailure) -> OpRefused { + match f { + StoreFailure::Conflict => OpRefused::Conflict, + StoreFailure::Failed(t) | StoreFailure::Refused(t) | StoreFailure::Fault(t) => { + OpRefused::Failed(t) + } + other => OpRefused::Failed(other.to_string()), + } +} + +fn text(f: StoreFailure) -> String { + match f { + StoreFailure::Failed(t) | StoreFailure::Refused(t) | StoreFailure::Fault(t) => t, + other => other.to_string(), + } +} + +impl TestStore { + /// The store on the connection string `url`. + pub(crate) fn open(url: &str) -> Result { + let settings = serde_json::json!({ "url": url }).to_string(); + Ok(Self { + store: open_loaded(&settings)?, + url: url.to_owned(), + raw: Mutex::new(None), + }) + } + + /// `StoreSlots::open` on `settings` (the settings' judge), with no host. + pub(crate) fn open_slot(settings: &[u8]) -> Result<(), String> { + ::open(settings, None).map(drop) + } + + /// An INDEPENDENT driver connection to the same database, for raw SQL. + pub(crate) fn lock(&self) -> RawGuard<'_> { + let mut g = self.raw.lock().unwrap_or_else(PoisonError::into_inner); + if g.is_none() { + *g = Some(super::connect_client_with_retry(&self.url)); + } + RawGuard(g) + } + + pub(crate) fn add_usage_op( + &self, + op: OpId, + bucket: &str, + window_start: u64, + delta: &UsageDelta, + ) -> Result<(), OpRefused> { + let cells = [(bucket, window_start, delta.clone())]; + block_on(StoreCalls::add_usage_batch(&self.store, op, &cells)).map_err(op_refused) + } + + pub(crate) fn append_audit_op(&self, op: OpId, entry: &AuditRecord) -> Result<(), OpRefused> { + block_on(StoreCalls::append_audit_batch( + &self.store, + op, + std::slice::from_ref(entry), + )) + .map_err(op_refused) + } + + pub(crate) fn reserve<'c>( + &self, + op: OpId, + epoch: u64, + cells: impl Iterator>, + grants: &mut impl Extend, + ) -> Result<(), ReserveRefused> { + let cells: Vec> = cells.collect(); + match block_on(StoreCalls::reserve(&self.store, op, epoch, &cells)) { + Ok(g) => { + grants.extend(g); + Ok(()) + } + Err(StoreFailure::Reserve(r)) => Err(r), + Err(StoreFailure::Conflict) => Err(ReserveRefused::Conflict), + Err(_) => Err(ReserveRefused::Unavailable), + } + } + + pub(crate) fn slice_release( + &self, + op: OpId, + epoch: u64, + items: impl Iterator, + released: &mut impl Extend, + ) -> Result<(), OpRefused> { + let items: Vec<(u64, u64)> = items.collect(); + let back = block_on(StoreCalls::slice_release(&self.store, op, epoch, &items)) + .map_err(op_refused)?; + released.extend(back); + Ok(()) + } + + pub(crate) fn window_caps(&self, op: OpId, caps: &[Cap<'_>]) -> Result<(), CapsRefused> { + block_on(StoreCalls::window_caps(&self.store, op, caps)).map_err(|f| match f { + StoreFailure::CapConflict(index) => CapsRefused::CapConflict { index }, + StoreFailure::Conflict => CapsRefused::Conflict, + other => CapsRefused::Failed(text(other)), + }) + } + + pub(crate) fn append_batch( + &self, + op: OpId, + stream: &str, + records: &[RecordBytes], + ) -> Result { + block_on(StoreCalls::append_batch(&self.store, op, stream, records)).map_err(op_refused) + } + + pub(crate) fn heads(&self) -> Result, String> { + block_on(StoreCalls::heads(&self.store)).map_err(text) + } + + pub(crate) fn session_put( + &self, + session: u64, + node: &str, + principal: &str, + ) -> Result<(), String> { + block_on(StoreCalls::session_put( + &self.store, + session, + node, + principal, + )) + .map_err(text) + } + + pub(crate) fn session_remove(&self, session: u64) -> Result<(), String> { + block_on(StoreCalls::session_remove(&self.store, session)).map_err(text) + } + + pub(crate) fn sessions_for(&self, principal: &str) -> Result, String> { + block_on(StoreCalls::sessions_for(&self.store, principal)).map_err(text) + } + + pub(crate) fn record_put(&self, schema: &str, key: &[u8], value: &[u8]) -> Result<(), String> { + let value = RecordBytes::new(value.to_vec()).map_err(|n| format!("{n} bytes"))?; + block_on(StoreCalls::record_put(&self.store, schema, key, &value)).map_err(text) + } + + pub(crate) fn record_get( + &self, + schema: &str, + key: &[u8], + ) -> Result, String> { + block_on(StoreCalls::record_get(&self.store, schema, key)).map_err(text) + } + + pub(crate) fn record_scan( + &self, + schema: &str, + prefix: &[u8], + limit: u32, + ) -> Result, RecordBytes)>, String> { + block_on(StoreCalls::record_scan(&self.store, schema, prefix, limit)).map_err(text) + } +} + +/// [`TestStore::lock`]'s connection. +pub(crate) struct RawGuard<'a>(MutexGuard<'a, Option>); + +impl std::ops::Deref for RawGuard<'_> { + type Target = postgres::Client; + fn deref(&self) -> &postgres::Client { + self.0.as_ref().expect("connected") + } +} + +impl std::ops::DerefMut for RawGuard<'_> { + fn deref_mut(&mut self) -> &mut postgres::Client { + self.0.as_mut().expect("connected") + } +} diff --git a/store-postgres/src/tests/plane_records.rs b/store-postgres/src/tests/plane_records.rs index 496ba96..47264d6 100644 --- a/store-postgres/src/tests/plane_records.rs +++ b/store-postgres/src/tests/plane_records.rs @@ -118,7 +118,7 @@ fn demotion(server: &str, reason: &str, recorded_at: u64) -> PlaneRecord { } /// Live Postgres is SHARED across tests, so each test owns its own ids and clears them first. -fn reset(store: &PostgresStore, kind: &str, ids: &[&str]) { +fn reset(store: &TestStore, kind: &str, ids: &[&str]) { let mut c = store.lock(); for id in ids { c.execute( @@ -134,7 +134,7 @@ fn reset(store: &PostgresStore, kind: &str, ids: &[&str]) { } } -fn reset_tokens(store: &PostgresStore, kind: &str, tokens: &[&str]) { +fn reset_tokens(store: &TestStore, kind: &str, tokens: &[&str]) { let mut c = store.lock(); for t in tokens { let _ = c.execute( @@ -146,7 +146,7 @@ fn reset_tokens(store: &PostgresStore, kind: &str, tokens: &[&str]) { /// Own the whole low band of a kind: a previous run's leftovers would otherwise be counted by the /// exact-count assertions the purge tests make. -fn clear_purge_band(store: &PostgresStore) { +fn clear_purge_band(store: &TestStore) { let top = clamp(PURGE_BAND_TOP); let mut c = store.lock(); c.execute( @@ -167,7 +167,7 @@ fn clear_purge_band(store: &PostgresStore) { .expect("clear the purge band's chains"); } -fn chain(store: &PostgresStore, kind: &str, parent: &str) -> Vec { +fn chain(store: &TestStore, kind: &str, parent: &str) -> Vec { store .list_plane_records(kind, &PlaneSelector::Parent(parent.to_string().into())) .unwrap() @@ -176,14 +176,14 @@ fn chain(store: &PostgresStore, kind: &str, parent: &str) -> Vec Option { +fn task_state(store: &TestStore, id: &str) -> Option { store .get_plane_record("task", id) .unwrap() .map(|b| decode(&b)["state"].as_str().unwrap().to_string()) } -fn listed_task_ids(store: &PostgresStore) -> Vec { +fn listed_task_ids(store: &TestStore) -> Vec { store .list_plane_records("task", &PlaneSelector::All) .unwrap() @@ -864,7 +864,7 @@ fn trust_ns(tag: &str) -> String { const TRUST_NOW: u64 = 2_000_000_000; -fn demotions(store: &PostgresStore) -> Vec { +fn demotions(store: &TestStore) -> Vec { store .list_plane_records("demotion", &PlaneSelector::All) .unwrap() diff --git a/store-postgres/src/tests/v160_shapes.rs b/store-postgres/src/tests/v160_shapes.rs index d93dc32..1a3fb06 100644 --- a/store-postgres/src/tests/v160_shapes.rs +++ b/store-postgres/src/tests/v160_shapes.rs @@ -608,7 +608,10 @@ fn metering_splits_on_priced_from_ms_and_carries_open_classes() { let Some(url) = live_url() else { return }; let store = connect_store_with_retry(&url).expect("connect"); let key_id = format!("vk_meter_units_{}", unique_suffix()); - let bucket = 20_270_601u64; + // Not 20_270_601: `purge_windows_and_metering_delete_only_what_is_older_than_the_boundary` + // writes and purges that whole bucket in parallel, and a whole-bucket purge here would take its + // row (a test-isolation race the slower per-op connections exposed). + let bucket = 20_270_611u64; let delta = |priced_from_ms: u64, input: u64, classes: &[(&str, u64)]| MeteringDelta { key_id: key_id.clone(), bucket, diff --git a/store-postgres/src/tests/v3_slots.rs b/store-postgres/src/tests/v3_slots.rs index f98f0d1..53ca52e 100644 --- a/store-postgres/src/tests/v3_slots.rs +++ b/store-postgres/src/tests/v3_slots.rs @@ -8,7 +8,7 @@ use super::*; use busbar_contract::abi::sdk::store::{ - Cap, CapsRefused, Cell, CellKey, Dimension, Grant, OpRefused, ReserveRefused, StoreSlots, + Cap, CapsRefused, Cell, CellKey, Dimension, Grant, OpRefused, ReserveRefused, }; use busbar_contract::abi::store::OpId; use busbar_contract::kinds::RecordBytes; @@ -33,7 +33,7 @@ fn op() -> OpId { OpId::from_parts(node, N.fetch_add(1, Ordering::Relaxed)) } -fn store(url: &str) -> PostgresStore { +fn store(url: &str) -> TestStore { connect_store_with_retry(url).expect("connect") } @@ -46,13 +46,13 @@ fn requests(bucket: &str) -> CellKey<'_> { } } -fn reserve(s: &PostgresStore, op: OpId, cells: &[Cell<'_>]) -> Result, ReserveRefused> { +fn reserve(s: &TestStore, op: OpId, cells: &[Cell<'_>]) -> Result, ReserveRefused> { let mut grants = Vec::new(); s.reserve(op, 0, cells.iter().copied(), &mut grants) .map(|()| grants) } -fn release(s: &PostgresStore, op: OpId, items: &[(u64, u64)]) -> Result, OpRefused> { +fn release(s: &TestStore, op: OpId, items: &[(u64, u64)]) -> Result, OpRefused> { let mut back = Vec::new(); s.slice_release(op, 0, items.iter().copied(), &mut back) .map(|()| back) @@ -70,12 +70,12 @@ fn open_refuses_settings_without_a_url() { &br#"{"url": " "}"#[..], ] { assert_eq!( - PostgresStore::open(settings).err().as_deref(), + TestStore::open_slot(settings).err().as_deref(), Some(want), "{settings:?}" ); } - let e = PostgresStore::open(b"{ not json").err().unwrap(); + let e = TestStore::open_slot(b"{ not json").err().unwrap(); assert!(e.starts_with("invalid postgres plugin config:"), "{e}"); } diff --git a/store-postgres/src/v3.rs b/store-postgres/src/v3.rs index 2a87d2d..9410062 100644 --- a/store-postgres/src/v3.rs +++ b/store-postgres/src/v3.rs @@ -21,25 +21,123 @@ //! never deadlock), tests each cell with 1.5.5's per-dimension rule (`abi::store::ReserveIn`) and //! applies all or nothing. A grant is valid until `u64::MAX`: nothing in the table expires a slice. //! +//! CONNECTIONS: every slot is ONE connection over the host's connector, run as straight-line async +//! code by the store SDK's `wire::drive` (busbar THE DESIGN: every call Ready or Pending(wake), no +//! socket of the plugin's own; ARCHITECT rulings 2026-10-03 on Q-L14-1 and Q-L16-2). The door +//! declares one outbound `tcp` need (`NEEDS`, `operator-infrastructure`); `open` parses the +//! settings, and its connect step reaches the server and ensures the schema, so an unreachable or +//! refusing server still fails the load at open, in the driver's words. +//! //! Every `u64` the ABI hands this module is stored BIT-FOR-BIT in a BIGINT (`as i64` / `as u64`) //! and every comparison runs here, never in SQL, so no value is clamped or wrapped on the way back. use std::sync::atomic::Ordering; +use busbar_contract::abi::host::conn::connector::{ + Need, DIRECTION_OUTBOUND, EGRESS_OPERATOR_INFRASTRUCTURE, KEEP_NAMED, +}; +use busbar_contract::abi::mechanism::call::{AbiStr, Blob, BLOB_OCTETS}; +use busbar_contract::abi::sdk::conn::Host; +use busbar_contract::abi::sdk::door::abi_str; +use busbar_contract::abi::sdk::store::wire::{drive, Body}; 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::kinds::{Head, RecordBytes}; use busbar_contract::records::{ - AuditRecord, MeteringDelta, PlaneRecordRef, RecordStoreError, UsageDelta, + AuditRecord, CredentialMeta, CredentialSecret, MeteringDelta, MeteringRow, PlaneRecordRef, + PlaneSelector, RecordStoreError, RecordStoreResult, UsageDelta, UsageLedger, VirtualKey, }; -use postgres::Transaction; -use crate::{now_secs, render_pg_error, PostgresStore, NAME}; +use crate::pgwire::Transaction; +use crate::{now_secs, render_pg_error, PostgresStore, Session, NAME}; + +const NO_TEXT: AbiStr = AbiStr { + ptr: std::ptr::null(), + len: 0, +}; -busbar_contract::store_door!(PostgresStore, NAME, env!("CARGO_PKG_VERSION"), 64); +/// THE STORE'S ONE NEED: the server, dialled over `tcp` at the DSN's `host:port` (the store names +/// the target per connection), in the `operator-infrastructure` egress class (private, loopback and +/// plaintext allowed; pinned; cloud metadata hosts refused). +pub const NEEDS: &[Need] = &[Need { + direction: DIRECTION_OUTBOUND, + egress_class: EGRESS_OPERATOR_INFRASTRUCTURE, + transport: abi_str("tcp"), + auth: NO_TEXT, + target_from: NO_TEXT, + trust_from: NO_TEXT, + details: Blob { + ptr: std::ptr::null(), + len: 0, + fmt: BLOB_OCTETS, + flags: 0, + }, + keep_response_headers: std::ptr::null(), + keep_response_headers_len: 0, + timeout_ms: 0, + keep_mode: KEEP_NAMED, + _reserved: 0, + deny_response_headers: std::ptr::null(), + deny_response_headers_len: 0, +}]; + +busbar_contract::store_door!( + PostgresStore, + NAME, + env!("CARGO_PKG_VERSION"), + 64, + needs: NEEDS +); + +/// RUN ONE OP over its own connection: open it (a failure is `$fail` of the driver's words), run +/// `$body` on the session `$s`, say goodbye. Every argument the body names is owned (the body runs +/// across the op's entries). +macro_rules! on_conn { + ($self:ident, $cx:ident, $fail:expr, |$s:ident| $body:expr) => {{ + let shared = $self.shared(); + drive($cx, move |wire| -> Body<_> { + Box::pin(async move { + let mut $s = match Session::open(wire, shared).await { + Ok(s) => s, + Err(e) => return Err(($fail)(e)), + }; + let answer = $body; + $s.close().await; + answer + }) + }) + }}; +} + +/// One `op_id`-carrying write in ONE transaction with its dedupe row: a replay answers the +/// original, a conflict applies nothing, and a new op runs `$apply` (its answer numbers, or `$E`) +/// and is remembered only if it applied (an error rolls the row back with the effect). +macro_rules! deduped { + ($s:expr, $op:expr, $body:expr, $conflict:expr, $backend:expr, $E:ty, |$tx:ident| $apply:block) => {{ + let op: OpId = $op; + let (mut tx, seen) = $s.dedupe_begin(op, $body).await.map_err($backend)?; + match seen { + Seen::Replay(answer) => answer, + Seen::Conflict => return Err($conflict), + Seen::New => { + let answer = { + let $tx = &mut tx; + typed::<$E, _>(async { $apply }).await? + }; + dedupe_finish(tx, op, &answer).await.map_err($backend)?; + answer + } + } + }}; +} + +/// Pin an apply block's error type. +fn typed, E>>>(f: F) -> F { + f +} /// How often (seconds) one instance sweeps `store_ops` past its retention. const SWEEP_EVERY_SECS: u64 = 60; @@ -54,7 +152,7 @@ fn unbits(v: i64) -> u64 { v as u64 } -fn pg(e: postgres::Error) -> String { +fn pg(e: crate::pgwire::Error) -> String { render_pg_error(&e) } @@ -114,7 +212,7 @@ enum Seen { } /// Claim `op` for `body` inside `tx`: insert its row, or read the row a committed call left. -fn claim(tx: &mut Transaction<'_>, op: OpId, body: &str) -> Result { +async fn claim(tx: &mut Transaction<'_>, op: OpId, body: &str) -> Result { let id: &[u8] = &op.0; let inserted = tx .execute( @@ -122,6 +220,7 @@ fn claim(tx: &mut Transaction<'_>, op: OpId, body: &str) -> Result ON CONFLICT (op_id) DO NOTHING", &[&id, &body, &bits(now_secs())], ) + .await .map_err(pg)?; if inserted == 1 { return Ok(Seen::New); @@ -131,6 +230,7 @@ fn claim(tx: &mut Transaction<'_>, op: OpId, body: &str) -> Result "SELECT body, answer FROM store_ops WHERE op_id = $1", &[&id], ) + .await .map_err(pg)?; Ok(match row { Some(r) if r.get::<_, &str>(0) == body => match decode(r.get(1)) { @@ -144,9 +244,21 @@ fn claim(tx: &mut Transaction<'_>, op: OpId, body: &str) -> Result }) } -impl PostgresStore { +/// Record a new op's answer on its dedupe row and commit the op's transaction. +async fn dedupe_finish(mut tx: Transaction<'_>, op: OpId, answer: &[u64]) -> Result<(), String> { + let id: &[u8] = &op.0; + tx.execute( + "UPDATE store_ops SET answer = $2 WHERE op_id = $1", + &[&id, &encode(answer)], + ) + .await + .map_err(pg)?; + tx.commit().await.map_err(pg) +} + +impl Session { /// Forget every `op_id` past its retention, at most once per [`SWEEP_EVERY_SECS`]. - fn sweep_ops(&self) { + async fn sweep_ops(&mut self) { let now = now_secs(); let last = self.ops_swept_at.load(Ordering::Relaxed); if now.saturating_sub(last) < SWEEP_EVERY_SECS @@ -160,178 +272,170 @@ impl PostgresStore { let before = bits(now.saturating_sub(OP_ID_RETENTION_SECS)); // Best effort: a failed sweep leaves rows that the next sweep takes. let _ = self - .lock() - .execute("DELETE FROM store_ops WHERE recorded_at < $1", &[&before]); + .lock_client() + .execute("DELETE FROM store_ops WHERE recorded_at < $1", &[&before]) + .await; } - /// Run one `op_id`-carrying write in ONE transaction with its dedupe row: a replay answers the - /// original, a conflict applies nothing, and a new op runs `apply` and is remembered only if it - /// applied (an error rolls the row back with the effect). - fn deduped( - &self, + /// Begin an `op_id`-carrying write: sweep, open its transaction, claim the op. + async fn dedupe_begin( + &mut self, op: OpId, body: &str, - conflict: E, - backend: fn(String) -> E, - apply: impl FnOnce(&mut Transaction<'_>) -> Result, E>, - ) -> Result, E> { - self.sweep_ops(); - let mut client = self.lock(); - let mut tx = client.transaction().map_err(|e| backend(pg(e)))?; - match claim(&mut tx, op, body).map_err(backend)? { - Seen::Replay(answer) => Ok(answer), - Seen::Conflict => Err(conflict), - Seen::New => { - let answer = apply(&mut tx)?; - let id: &[u8] = &op.0; - tx.execute( - "UPDATE store_ops SET answer = $2 WHERE op_id = $1", - &[&id, &encode(&answer)], - ) - .map_err(|e| backend(pg(e)))?; - tx.commit().map_err(|e| backend(pg(e)))?; - Ok(answer) - } - } + ) -> Result<(Transaction<'_>, Seen), String> { + self.sweep_ops().await; + let mut tx = self.lock_client().transaction().await.map_err(pg)?; + let seen = claim(&mut tx, op, body).await?; + Ok((tx, seen)) } - fn op( - &self, - op: OpId, - body: &str, - apply: impl FnOnce(&mut Transaction<'_>) -> OpResult<()>, - ) -> OpResult<()> { - self.deduped(op, body, OpRefused::Conflict, OpRefused::Failed, |tx| { - apply(tx).map(|()| Vec::new()) - }) - .map(drop) - } -} - -/// An audit append inside an op's transaction: a vanished conflicting row is a failure here (the -/// transaction cannot be retried from inside), never a success. -fn audit_in(tx: &mut Transaction<'_>, entry: &AuditRecord) -> OpResult<()> { - match PostgresStore::append_audit_in(tx, entry).map_err(failed)? { - true => Ok(()), - false => Err(OpRefused::Failed(format!( - "append_audit: seq {} was freed between the insert and the read-back; the record was \ - NOT stored", - entry.seq - ))), - } -} - -impl StoreSlots for PostgresStore { - const TAIL: Tail = Tail { - ephemeral: false, - durable_plane: true, - fork_refusal: true, - }; - - /// Open from the store section's settings: - /// - /// ```json - /// { "url": "postgres://user:pass@host:5432/busbar" } - /// ``` - fn open(settings: &[u8]) -> Result { - let v: serde_json::Value = if settings.trim_ascii().is_empty() { - serde_json::Value::Object(Default::default()) - } else { - serde_json::from_slice(settings) - .map_err(|e| format!("invalid postgres plugin config: {e}"))? - }; - let url = v - .get("url") - .and_then(|x| x.as_str()) - .map(str::trim) - .filter(|s| !s.is_empty()) - .ok_or_else(|| { - "postgres plugin config requires a \"url\" (a libpq connection string)".to_string() - })?; - PostgresStore::connect(url).map_err(|e| e.0) - } - - fn add_usage_op( - &self, + async fn v3_add_usage( + &mut self, op: OpId, bucket: &str, window_start: u64, delta: &UsageDelta, ) -> OpResult<()> { let body = format!("add_usage:{bucket:?}:{window_start}:{delta:?}"); - self.op(op, &body, |tx| { - Self::add_usage_in(tx, bucket, window_start, delta).map_err(failed) - }) + deduped!( + self, + op, + &body, + OpRefused::Conflict, + OpRefused::Failed, + OpRefused, + |tx| { + Session::add_usage_in(tx, bucket, window_start, delta) + .await + .map_err(failed)?; + Ok(Vec::new()) + } + ); + Ok(()) } - fn add_metering_op(&self, op: OpId, delta: &MeteringDelta) -> OpResult<()> { + async fn v3_add_metering(&mut self, op: OpId, delta: &MeteringDelta) -> OpResult<()> { let body = format!("add_metering:{delta:?}"); - self.op(op, &body, |tx| { - Self::add_metering_in(tx, delta).map_err(failed) - }) + deduped!( + self, + op, + &body, + OpRefused::Conflict, + OpRefused::Failed, + OpRefused, + |tx| { + Session::add_metering_in(tx, delta).await.map_err(failed)?; + Ok(Vec::new()) + } + ); + Ok(()) } - fn append_audit_op(&self, op: OpId, entry: &AuditRecord) -> OpResult<()> { + async fn v3_append_audit(&mut self, op: OpId, entry: &AuditRecord) -> OpResult<()> { let body = format!("append_audit:{entry:?}"); - self.op(op, &body, |tx| audit_in(tx, entry)) + deduped!( + self, + op, + &body, + OpRefused::Conflict, + OpRefused::Failed, + OpRefused, + |tx| { + audit_in(tx, entry).await?; + Ok(Vec::new()) + } + ); + Ok(()) } - fn append_plane_record_op(&self, op: OpId, record: PlaneRecordRef<'_>) -> OpResult<()> { - let record = record.to_record(); + async fn v3_append_plane_record( + &mut self, + op: OpId, + record: &busbar_contract::records::PlaneRecord, + ) -> OpResult<()> { let body = format!("append_plane_record:{record:?}"); - self.op(op, &body, |tx| { - match Self::append_plane_record_in(tx, &record).map_err(failed)? { - true => Ok(()), - false => Err(OpRefused::Failed(format!( + deduped!( + self, + op, + &body, + OpRefused::Conflict, + OpRefused::Failed, + OpRefused, + |tx| { + match Session::append_plane_record_in(tx, record) + .await + .map_err(failed)? + { + true => Ok(Vec::new()), + false => Err(OpRefused::Failed(format!( "append_plane_record: kind '{}' seq {} was freed between the insert and the \ read-back; the record was NOT stored", record.kind, record.seq ))), + } } - }) + ); + Ok(()) } - fn append_batch(&self, op: OpId, stream: &str, records: &[RecordBytes]) -> OpResult { + async fn v3_append_batch( + &mut self, + op: OpId, + stream: &str, + records: &[RecordBytes], + ) -> OpResult { let body = format!("append_batch:{stream:?}:{records:?}"); - let answer = self.deduped(op, &body, OpRefused::Conflict, OpRefused::Failed, |tx| { - // One appender per stream at a time, fleet-wide: the head is read and extended under - // the stream's transaction-scoped advisory lock. - tx.execute( - "SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", - &[&stream], - ) - .map_err(|e| OpRefused::Failed(pg(e)))?; - let head: i64 = tx - .query_one( - "SELECT COALESCE(MAX(seq), 0) FROM store_journal WHERE stream = $1", - &[&stream], - ) - .map_err(|e| OpRefused::Failed(pg(e)))? - .get(0); - let mut seq = head; - for r in records { - seq += 1; + let answer = deduped!( + self, + op, + &body, + OpRefused::Conflict, + OpRefused::Failed, + OpRefused, + |tx| { + // One appender per stream at a time, fleet-wide: the head is read and extended under + // the stream's transaction-scoped advisory lock. tx.execute( - "INSERT INTO store_journal (stream, seq, record) VALUES ($1, $2, $3)", - &[&stream, &seq, &r.as_slice()], + "SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", + &[&stream], ) + .await .map_err(|e| OpRefused::Failed(pg(e)))?; + let head: i64 = tx + .query_one( + "SELECT COALESCE(MAX(seq), 0) FROM store_journal WHERE stream = $1", + &[&stream], + ) + .await + .map_err(|e| OpRefused::Failed(pg(e)))? + .get(0); + let mut seq = head; + for r in records { + seq += 1; + tx.execute( + "INSERT INTO store_journal (stream, seq, record) VALUES ($1, $2, $3)", + &[&stream, &seq, &r.as_slice()], + ) + .await + .map_err(|e| OpRefused::Failed(pg(e)))?; + } + Ok(vec![unbits(seq), 0]) } - Ok(vec![unbits(seq), 0]) - })?; + ); match answer[..] { [seq, epoch] => Ok(Head { seq, epoch }), _ => Err(OpRefused::Conflict), } } - fn heads(&self) -> Result, String> { + async fn v3_heads(&mut self) -> Result, String> { let rows = self - .lock() + .lock_client() .query( "SELECT stream, MAX(seq) FROM store_journal GROUP BY stream ORDER BY stream", &[], ) + .await .map_err(pg)?; Ok(rows .iter() @@ -347,35 +451,43 @@ impl StoreSlots for PostgresStore { .collect()) } - fn session_put(&self, session: u64, node: &str, principal: &str) -> Result<(), String> { - self.lock() + async fn v3_session_put( + &mut self, + session: u64, + node: &str, + principal: &str, + ) -> Result<(), String> { + self.lock_client() .execute( "INSERT INTO store_sessions (session, node, principal) VALUES ($1, $2, $3) ON CONFLICT (session) DO UPDATE SET node = EXCLUDED.node, principal = EXCLUDED.principal", &[&bits(session), &node, &principal], ) + .await .map(drop) .map_err(pg) } - fn session_remove(&self, session: u64) -> Result<(), String> { - self.lock() + async fn v3_session_remove(&mut self, session: u64) -> Result<(), String> { + self.lock_client() .execute( "DELETE FROM store_sessions WHERE session = $1", &[&bits(session)], ) + .await .map(drop) .map_err(pg) } - fn sessions_for(&self, principal: &str) -> Result, String> { + async fn v3_sessions_for(&mut self, principal: &str) -> Result, String> { let rows = self - .lock() + .lock_client() .query( "SELECT session, node FROM store_sessions WHERE principal = $1", &[&principal], ) + .await .map_err(pg)?; let mut sessions: Vec<(u64, String)> = rows.iter().map(|r| (unbits(r.get(0)), r.get(1))).collect(); @@ -383,40 +495,51 @@ impl StoreSlots for PostgresStore { Ok(sessions) } - fn record_put(&self, schema: &str, key: &[u8], value: &[u8]) -> Result<(), String> { - self.lock() + async fn v3_record_put( + &mut self, + schema: &str, + key: &[u8], + value: &[u8], + ) -> Result<(), String> { + self.lock_client() .execute( "INSERT INTO store_records (schema_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (schema_id, key) DO UPDATE SET value = EXCLUDED.value", &[&schema, &key, &value], ) + .await .map(drop) .map_err(pg) } - fn record_get(&self, schema: &str, key: &[u8]) -> Result, String> { + async fn v3_record_get( + &mut self, + schema: &str, + key: &[u8], + ) -> Result, String> { let row = self - .lock() + .lock_client() .query_opt( "SELECT value FROM store_records WHERE schema_id = $1 AND key = $2", &[&schema, &key], ) + .await .map_err(pg)?; row.map(|r| record(r.get(0))).transpose() } - fn record_scan( - &self, + async fn v3_record_scan( + &mut self, schema: &str, prefix: &[u8], limit: u32, - ) -> Result, RecordBytes)>, String> { + ) -> Result { // `limit` 0 is nothing, never everything. if limit == 0 { return Ok(Vec::new()); } let rows = self - .lock() + .lock_client() .query( "SELECT key, value FROM store_records WHERE schema_id = $1 @@ -424,210 +547,293 @@ impl StoreSlots for PostgresStore { ORDER BY key LIMIT $3", &[&schema, &prefix, &i64::from(limit)], ) + .await .map_err(pg)?; rows.iter() .map(|r| Ok((r.get(0), record(r.get(1))?))) .collect() } - fn reserve<'c>( - &self, + /// `reserve` over the cells' slots (each cell's slot key, its dimension and its amount). + async fn v3_reserve( + &mut self, op: OpId, - epoch: u64, - cells: impl Iterator> + Clone, - grants: &mut impl Extend, - ) -> Result<(), ReserveRefused> { - let cells: Vec> = cells.collect(); - let body = format!("reserve:{epoch}:{cells:?}"); + body: &str, + cells: &[(String, Dimension<'static>, u64)], + ) -> Result, ReserveRefused> { let unavailable = |_: String| ReserveRefused::Unavailable; - let answer = self.deduped(op, &body, ReserveRefused::Conflict, unavailable, |tx| { - let slots: Vec = cells.iter().map(|c| slot(&c.key)).collect(); - let rows = tx - .query( - "SELECT slot, cap, used FROM money_slots WHERE slot = ANY($1) + let answer = deduped!( + self, + op, + body, + ReserveRefused::Conflict, + unavailable, + ReserveRefused, + |tx| { + let slots: Vec = cells.iter().map(|c| c.0.clone()).collect(); + let rows = tx + .query( + "SELECT slot, cap, used FROM money_slots WHERE slot = ANY($1) ORDER BY slot FOR UPDATE", - &[&slots], - ) - .map_err(|_| ReserveRefused::Unavailable)?; - let mut held: std::collections::HashMap = rows - .iter() - .map(|r| (r.get(0), (unbits(r.get(1)), unbits(r.get(2))))) - .collect(); - // The chain draw is all or nothing: each cell is tested against what the cells before - // it in THIS draw add, and nothing is written until every cell passes. - for (i, (c, s)) in cells.iter().zip(&slots).enumerate() { - let Some((cap, used)) = held.get_mut(s) else { - return Err(ReserveRefused::NoCap { cell: i as u32 }); - }; - if exhausted(&c.key.dimension, *used, c.amount, *cap) { - return Err(ReserveRefused::Exhausted { cell: i as u32 }); + &[&slots], + ) + .await + .map_err(|_| ReserveRefused::Unavailable)?; + let mut held: std::collections::HashMap = rows + .iter() + .map(|r| (r.get(0), (unbits(r.get(1)), unbits(r.get(2))))) + .collect(); + // The chain draw is all or nothing: each cell is tested against what the cells before + // it in THIS draw add, and nothing is written until every cell passes. + for (i, (s, dimension, amount)) in cells.iter().enumerate() { + let Some((cap, used)) = held.get_mut(s) else { + return Err(ReserveRefused::NoCap { cell: i as u32 }); + }; + if exhausted(dimension, *used, *amount, *cap) { + return Err(ReserveRefused::Exhausted { cell: i as u32 }); + } + *used = used.saturating_add(*amount); } - *used = used.saturating_add(c.amount); - } - let mut answer = Vec::with_capacity(cells.len() * 3); - for (c, s) in cells.iter().zip(&slots) { - tx.execute( - "UPDATE money_slots SET used = $2 WHERE slot = $1", - &[s, &bits(held[s].1)], - ) - .map_err(|_| ReserveRefused::Unavailable)?; - let id: i64 = tx - .query_one( - "INSERT INTO money_slices (slot, remaining) VALUES ($1, $2) - RETURNING slice_id", - &[s, &bits(c.amount)], + let mut answer = Vec::with_capacity(cells.len() * 3); + for (s, _, amount) in cells { + tx.execute( + "UPDATE money_slots SET used = $2 WHERE slot = $1", + &[s, &bits(held[s].1)], ) - .map_err(|_| ReserveRefused::Unavailable)? - .get(0); - answer.extend([unbits(id), c.amount, u64::MAX]); + .await + .map_err(|_| ReserveRefused::Unavailable)?; + let id: i64 = tx + .query_one( + "INSERT INTO money_slices (slot, remaining) VALUES ($1, $2) + RETURNING slice_id", + &[s, &bits(*amount)], + ) + .await + .map_err(|_| ReserveRefused::Unavailable)? + .get(0); + answer.extend([unbits(id), *amount, u64::MAX]); + } + Ok(answer) } - Ok(answer) - })?; + ); if answer.len() != cells.len() * 3 { return Err(ReserveRefused::Conflict); } - grants.extend(answer.as_chunks::<3>().0.iter().map(|g| Grant { - slice_id: g[0], - granted: g[1], - valid_until_ms: g[2], - })); - Ok(()) + Ok(answer + .as_chunks::<3>() + .0 + .iter() + .map(|g| Grant { + slice_id: g[0], + granted: g[1], + valid_until_ms: g[2], + }) + .collect()) } - fn slice_release( - &self, + async fn v3_slice_release( + &mut self, op: OpId, - epoch: u64, - items: impl Iterator + Clone, - released: &mut impl Extend, - ) -> OpResult<()> { - let items: Vec<(u64, u64)> = items.collect(); - let body = format!("slice_release:{epoch}:{items:?}"); - let answer = self.deduped(op, &body, OpRefused::Conflict, OpRefused::Failed, |tx| { - let mut back_all = Vec::with_capacity(items.len()); - let mut seen = std::collections::HashSet::new(); - for &(id, unspent) in &items { - let row = tx + body: &str, + items: &[(u64, u64)], + ) -> OpResult> { + let answer = deduped!( + self, + op, + body, + OpRefused::Conflict, + OpRefused::Failed, + OpRefused, + |tx| { + let mut back_all = Vec::with_capacity(items.len()); + let mut seen = std::collections::HashSet::new(); + for &(id, unspent) in items { + let row = tx .query_opt( "SELECT slot, remaining FROM money_slices WHERE slice_id = $1 FOR UPDATE", &[&bits(id)], ) + .await .map_err(|e| OpRefused::Failed(pg(e)))?; - let Some(row) = row else { - // An item naming a slice an EARLIER item of this call closed answers 0; any - // other unknown slice fails the whole release. - if seen.contains(&id) { - back_all.push(0); - continue; + let Some(row) = row else { + // An item naming a slice an EARLIER item of this call closed answers 0; any + // other unknown slice fails the whole release. + if seen.contains(&id) { + back_all.push(0); + continue; + } + return Err(OpRefused::Failed(format!( + "slice_release: slice {id} is not held" + ))); + }; + seen.insert(id); + let s: String = row.get(0); + let left = unbits(row.get(1)); + let back = unspent.min(left); + let closed = if left == back { + tx.execute("DELETE FROM money_slices WHERE slice_id = $1", &[&bits(id)]) + .await + } else { + tx.execute( + "UPDATE money_slices SET remaining = $2 WHERE slice_id = $1", + &[&bits(id), &bits(left - back)], + ) + .await + }; + closed.map_err(|e| OpRefused::Failed(pg(e)))?; + let used = tx + .query_opt( + "SELECT used FROM money_slots WHERE slot = $1 FOR UPDATE", + &[&s], + ) + .await + .map_err(|e| OpRefused::Failed(pg(e)))?; + if let Some(used) = used { + tx.execute( + "UPDATE money_slots SET used = $2 WHERE slot = $1", + &[&s, &bits(unbits(used.get(0)).saturating_sub(back))], + ) + .await + .map_err(|e| OpRefused::Failed(pg(e)))?; } - return Err(OpRefused::Failed(format!( - "slice_release: slice {id} is not held" - ))); - }; - seen.insert(id); - let s: String = row.get(0); - let left = unbits(row.get(1)); - let back = unspent.min(left); - let closed = if left == back { - tx.execute("DELETE FROM money_slices WHERE slice_id = $1", &[&bits(id)]) - } else { - tx.execute( - "UPDATE money_slices SET remaining = $2 WHERE slice_id = $1", - &[&bits(id), &bits(left - back)], - ) - }; - closed.map_err(|e| OpRefused::Failed(pg(e)))?; - let used = tx - .query_opt( - "SELECT used FROM money_slots WHERE slot = $1 FOR UPDATE", - &[&s], - ) - .map_err(|e| OpRefused::Failed(pg(e)))?; - if let Some(used) = used { - tx.execute( - "UPDATE money_slots SET used = $2 WHERE slot = $1", - &[&s, &bits(unbits(used.get(0)).saturating_sub(back))], - ) - .map_err(|e| OpRefused::Failed(pg(e)))?; + back_all.push(back); } - back_all.push(back); + Ok(back_all) } - Ok(back_all) - })?; + ); if answer.len() != items.len() { return Err(OpRefused::Conflict); } - released.extend(answer); - Ok(()) + Ok(answer) } - fn 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 { - Self::add_usage_in(tx, bucket, *window, delta).map_err(failed)?; + async fn v3_add_usage_batch( + &mut self, + op: OpId, + body: &str, + cells: &[(String, u64, UsageDelta)], + ) -> OpResult<()> { + deduped!( + self, + op, + body, + OpRefused::Conflict, + OpRefused::Failed, + OpRefused, + |tx| { + for (bucket, window, delta) in cells { + Session::add_usage_in(tx, bucket, *window, delta) + .await + .map_err(failed)?; + } + Ok(Vec::new()) } - Ok(()) - }) + ); + Ok(()) } - fn add_metering_batch(&self, op: OpId, deltas: &[MeteringDelta]) -> OpResult<()> { + async fn v3_add_metering_batch(&mut self, op: OpId, deltas: &[MeteringDelta]) -> OpResult<()> { let body = format!("add_metering_batch:{deltas:?}"); - self.op(op, &body, |tx| { - for d in deltas { - Self::add_metering_in(tx, d).map_err(failed)?; + deduped!( + self, + op, + &body, + OpRefused::Conflict, + OpRefused::Failed, + OpRefused, + |tx| { + for d in deltas { + Session::add_metering_in(tx, d).await.map_err(failed)?; + } + Ok(Vec::new()) } - Ok(()) - }) + ); + Ok(()) } - fn append_audit_batch(&self, op: OpId, entries: &[AuditRecord]) -> OpResult<()> { + async fn v3_append_audit_batch(&mut self, op: OpId, entries: &[AuditRecord]) -> OpResult<()> { let body = format!("append_audit_batch:{entries:?}"); // One transaction: a fork anywhere (against a stored seq or an earlier entry of this batch) // rolls the whole batch back. - self.op(op, &body, |tx| { - for e in entries { - audit_in(tx, e)?; + deduped!( + self, + op, + &body, + OpRefused::Conflict, + OpRefused::Failed, + OpRefused, + |tx| { + for e in entries { + audit_in(tx, e).await?; + } + Ok(Vec::new()) } - Ok(()) - }) + ); + Ok(()) } - fn window_caps(&self, op: OpId, caps: &[Cap<'_>]) -> Result<(), CapsRefused> { - let body = format!("window_caps:{caps:?}"); - self.deduped(op, &body, CapsRefused::Conflict, CapsRefused::Failed, |tx| { - let fail = |e: postgres::Error| CapsRefused::Failed(pg(e)); - // Atomic per push: every cap is checked (against the stored cap and the ones before it - // in this push) before the push's transaction commits; the first conflict rolls it all - // back. - for (index, c) in caps.iter().enumerate() { - let s = slot(&c.key); - let stored = tx - .query_opt( - "SELECT cap, config_gen FROM money_slots WHERE slot = $1 FOR UPDATE", - &[&s], - ) - .map_err(fail)? - .map(|r| (unbits(r.get(0)), unbits(r.get(1)))); - match stored { - Some((cap, gen)) if gen == c.config_gen && cap != c.cap => { - return Err(CapsRefused::CapConflict { index }); - } - Some((_, gen)) if gen >= c.config_gen => {} - _ => { - tx.execute( + /// `window_caps` over the caps' slots (each cap's slot key, cap and config generation). + async fn v3_window_caps( + &mut self, + op: OpId, + body: &str, + caps: &[(String, u64, u64)], + ) -> Result<(), CapsRefused> { + deduped!( + self, + op, + body, + CapsRefused::Conflict, + CapsRefused::Failed, + CapsRefused, + |tx| { + let fail = |e: crate::pgwire::Error| CapsRefused::Failed(pg(e)); + // Atomic per push: every cap is checked (against the stored cap and the ones before it + // in this push) before the push's transaction commits; the first conflict rolls it all + // back. + for (index, (s, cap_value, config_gen)) in caps.iter().enumerate() { + let stored = tx + .query_opt( + "SELECT cap, config_gen FROM money_slots WHERE slot = $1 FOR UPDATE", + &[s], + ) + .await + .map_err(fail)? + .map(|r| (unbits(r.get(0)), unbits(r.get(1)))); + match stored { + Some((cap, gen)) if gen == *config_gen && cap != *cap_value => { + return Err(CapsRefused::CapConflict { index }); + } + Some((_, gen)) if gen >= *config_gen => {} + _ => { + tx.execute( "INSERT INTO money_slots (slot, cap, config_gen, used) VALUES ($1, $2, $3, 0) ON CONFLICT (slot) DO UPDATE SET cap = EXCLUDED.cap, config_gen = EXCLUDED.config_gen", - &[&s, &bits(c.cap), &bits(c.config_gen)], + &[s, &bits(*cap_value), &bits(*config_gen)], ) + .await .map_err(fail)?; + } } } + Ok(Vec::new()) } - Ok(Vec::new()) - }) - .map(drop) + ); + Ok(()) + } +} + +/// An audit append inside an op's transaction: a vanished conflicting row is a failure here (the +/// transaction cannot be retried from inside), never a success. +async fn audit_in(tx: &mut Transaction<'_>, entry: &AuditRecord) -> OpResult<()> { + match Session::append_audit_in(tx, entry).await.map_err(failed)? { + true => Ok(()), + false => Err(OpRefused::Failed(format!( + "append_audit: seq {} was freed between the insert and the read-back; the record was \ + NOT stored", + entry.seq + ))), } } @@ -635,3 +841,578 @@ impl StoreSlots for PostgresStore { fn record(bytes: Vec) -> Result { RecordBytes::new(bytes).map_err(|n| format!("a stored record of {n} bytes is over the ceiling")) } + +/// The store section's settings: its `url`. Shared by `validate` and `open`, so their refusals are +/// the same words. +fn settings_url(settings: &[u8]) -> Result { + let v: serde_json::Value = if settings.trim_ascii().is_empty() { + serde_json::Value::Object(Default::default()) + } else { + serde_json::from_slice(settings) + .map_err(|e| format!("invalid postgres plugin config: {e}"))? + }; + v.get("url") + .and_then(|x| x.as_str()) + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(str::to_owned) + .ok_or_else(|| { + "postgres plugin config requires a \"url\" (a libpq connection string)".to_string() + }) +} + +/// A dimension, owned for an op that runs across entries (only its variant is read). +fn dimension_kind(d: &Dimension<'_>) -> Dimension<'static> { + match d { + Dimension::NanoUnits => Dimension::NanoUnits, + Dimension::Requests => Dimension::Requests, + Dimension::Concurrency => Dimension::Concurrency, + Dimension::Class(_) => Dimension::Class(""), + } +} + +impl StoreSlots for PostgresStore { + const TAIL: Tail = Tail { + ephemeral: false, + durable_plane: true, + fork_refusal: true, + }; + + fn validate(settings: &[u8]) -> Result<(), String> { + settings_url(settings).map(drop) + } + + /// Open from the store section's settings: + /// + /// ```json + /// { "url": "postgres://user:pass@host:5432/busbar" } + /// ``` + /// + /// The settings are parsed here; the server is reached by the connect step. + fn open(settings: &[u8], _host: Option) -> Result { + let url = settings_url(settings)?; + PostgresStore::new(&url).map_err(|e| e.0) + } + + /// Reach the server and ensure the schema (1.5.5's connect + migrate, at the same moment: the + /// load), in the driver's words when it cannot. + fn connect(&self, cx: &mut Op<'_>) -> Step> { + on_conn!(self, cx, |e| e, |s| s.migrate().await.map_err(|e| e.0)) + } + + fn add_usage_op( + &self, + cx: &mut Op<'_>, + op: OpId, + bucket: &str, + window_start: u64, + delta: &UsageDelta, + ) -> Step> { + let (bucket, delta) = (bucket.to_owned(), delta.clone()); + on_conn!(self, cx, OpRefused::Failed, |s| s + .v3_add_usage(op, &bucket, window_start, &delta) + .await) + } + + fn add_metering_op( + &self, + cx: &mut Op<'_>, + op: OpId, + delta: &MeteringDelta, + ) -> Step> { + let delta = delta.clone(); + on_conn!(self, cx, OpRefused::Failed, |s| s + .v3_add_metering(op, &delta) + .await) + } + + fn append_audit_op( + &self, + cx: &mut Op<'_>, + op: OpId, + entry: &AuditRecord, + ) -> Step> { + let entry = entry.clone(); + on_conn!(self, cx, OpRefused::Failed, |s| s + .v3_append_audit(op, &entry) + .await) + } + + fn append_plane_record_op( + &self, + cx: &mut Op<'_>, + op: OpId, + record: PlaneRecordRef<'_>, + ) -> Step> { + let record = record.to_record(); + on_conn!(self, cx, OpRefused::Failed, |s| s + .v3_append_plane_record(op, &record) + .await) + } + + fn append_batch( + &self, + cx: &mut Op<'_>, + op: OpId, + stream: &str, + records: &[RecordBytes], + ) -> Step> { + let (stream, records) = (stream.to_owned(), records.to_vec()); + on_conn!(self, cx, OpRefused::Failed, |s| s + .v3_append_batch(op, &stream, &records) + .await) + } + + fn heads(&self, cx: &mut Op<'_>) -> Step, String>> { + on_conn!(self, cx, |e| e, |s| s.v3_heads().await) + } + + fn session_put( + &self, + cx: &mut Op<'_>, + session: u64, + node: &str, + principal: &str, + ) -> Step> { + let (node, principal) = (node.to_owned(), principal.to_owned()); + on_conn!(self, cx, |e| e, |s| s + .v3_session_put(session, &node, &principal) + .await) + } + + fn session_remove(&self, cx: &mut Op<'_>, session: u64) -> Step> { + on_conn!(self, cx, |e| e, |s| s.v3_session_remove(session).await) + } + + fn sessions_for( + &self, + cx: &mut Op<'_>, + principal: &str, + ) -> Step, String>> { + let principal = principal.to_owned(); + on_conn!(self, cx, |e| e, |s| s.v3_sessions_for(&principal).await) + } + + fn record_put( + &self, + cx: &mut Op<'_>, + schema: &str, + key: &[u8], + value: &[u8], + ) -> Step> { + let (schema, key, value) = (schema.to_owned(), key.to_vec(), value.to_vec()); + on_conn!(self, cx, |e| e, |s| s + .v3_record_put(&schema, &key, &value) + .await) + } + + fn record_get( + &self, + cx: &mut Op<'_>, + schema: &str, + key: &[u8], + ) -> Step, String>> { + let (schema, key) = (schema.to_owned(), key.to_vec()); + on_conn!(self, cx, |e| e, |s| s.v3_record_get(&schema, &key).await) + } + + fn record_scan( + &self, + cx: &mut Op<'_>, + schema: &str, + prefix: &[u8], + limit: u32, + ) -> Step> { + let (schema, prefix) = (schema.to_owned(), prefix.to_vec()); + on_conn!(self, cx, |e| e, |s| s + .v3_record_scan(&schema, &prefix, limit) + .await) + } + + fn reserve<'c>( + &self, + cx: &mut Op<'_>, + op: OpId, + epoch: u64, + cells: impl Iterator> + Clone, + grants: &mut impl Extend, + ) -> Step> { + let cells: Vec> = cells.collect(); + let body = format!("reserve:{epoch}:{cells:?}"); + let owned: Vec<(String, Dimension<'static>, u64)> = cells + .iter() + .map(|c| (slot(&c.key), dimension_kind(&c.key.dimension), c.amount)) + .collect(); + let step = on_conn!(self, cx, |_: String| ReserveRefused::Unavailable, |s| s + .v3_reserve(op, &body, &owned) + .await); + match step { + Step::Ready(Ok(g)) => { + grants.extend(g); + Step::Ready(Ok(())) + } + Step::Ready(Err(e)) => Step::Ready(Err(e)), + Step::Pending { wake_at_ns } => Step::Pending { wake_at_ns }, + } + } + + fn slice_release( + &self, + cx: &mut Op<'_>, + op: OpId, + epoch: u64, + items: impl Iterator + Clone, + released: &mut impl Extend, + ) -> Step> { + let items: Vec<(u64, u64)> = items.collect(); + let body = format!("slice_release:{epoch}:{items:?}"); + let step = on_conn!(self, cx, OpRefused::Failed, |s| s + .v3_slice_release(op, &body, &items) + .await); + match step { + Step::Ready(Ok(back)) => { + released.extend(back); + Step::Ready(Ok(())) + } + Step::Ready(Err(e)) => Step::Ready(Err(e)), + Step::Pending { wake_at_ns } => Step::Pending { wake_at_ns }, + } + } + + fn add_usage_batch( + &self, + cx: &mut Op<'_>, + op: OpId, + cells: &[(&str, u64, UsageDelta)], + ) -> Step> { + let body = format!("add_usage_batch:{cells:?}"); + let cells: Vec<(String, u64, UsageDelta)> = cells + .iter() + .map(|(b, w, d)| ((*b).to_owned(), *w, d.clone())) + .collect(); + on_conn!(self, cx, OpRefused::Failed, |s| s + .v3_add_usage_batch(op, &body, &cells) + .await) + } + + fn add_metering_batch( + &self, + cx: &mut Op<'_>, + op: OpId, + deltas: &[MeteringDelta], + ) -> Step> { + let deltas = deltas.to_vec(); + on_conn!(self, cx, OpRefused::Failed, |s| s + .v3_add_metering_batch(op, &deltas) + .await) + } + + fn append_audit_batch( + &self, + cx: &mut Op<'_>, + op: OpId, + entries: &[AuditRecord], + ) -> Step> { + let entries = entries.to_vec(); + on_conn!(self, cx, OpRefused::Failed, |s| s + .v3_append_audit_batch(op, &entries) + .await) + } + + fn window_caps( + &self, + cx: &mut Op<'_>, + op: OpId, + caps: &[Cap<'_>], + ) -> Step> { + let body = format!("window_caps:{caps:?}"); + let caps: Vec<(String, u64, u64)> = caps + .iter() + .map(|c| (slot(&c.key), c.cap, c.config_gen)) + .collect(); + on_conn!(self, cx, CapsRefused::Failed, |s| s + .v3_window_caps(op, &body, &caps) + .await) + } + + // ── the 1.5.5 op set (slots 0-32): each the 1.5.5 body, on the op's connection ────────── + + fn put_key(&self, cx: &mut Op<'_>, key: &VirtualKey) -> Step> { + let key = key.clone(); + on_conn!(self, cx, RecordStoreError, |s| s.put_key(&key).await) + } + + fn get_key(&self, cx: &mut Op<'_>, id: &str) -> Step>> { + let id = id.to_owned(); + on_conn!(self, cx, RecordStoreError, |s| s.get_key(&id).await) + } + + fn list_keys(&self, cx: &mut Op<'_>) -> Step>> { + on_conn!(self, cx, RecordStoreError, |s| s.list_keys().await) + } + + fn delete_key(&self, cx: &mut Op<'_>, id: &str) -> Step> { + let id = id.to_owned(); + on_conn!(self, cx, RecordStoreError, |s| s.delete_key(&id).await) + } + + fn scrub_key(&self, cx: &mut Op<'_>, id: &str) -> Step> { + let id = id.to_owned(); + on_conn!(self, cx, RecordStoreError, |s| s.scrub_key(&id).await) + } + + fn list_keys_since( + &self, + cx: &mut Op<'_>, + since: u64, + ) -> Step>> { + on_conn!(self, cx, RecordStoreError, |s| s + .list_keys_since(since) + .await) + } + + fn get_usage( + &self, + cx: &mut Op<'_>, + bucket_id: &str, + window_start: u64, + ) -> Step> { + let bucket_id = bucket_id.to_owned(); + on_conn!(self, cx, RecordStoreError, |s| s + .get_usage(&bucket_id, window_start) + .await) + } + + fn put_usage( + &self, + cx: &mut Op<'_>, + bucket_id: &str, + window_start: u64, + ledger: &UsageLedger, + ) -> Step> { + let (bucket_id, ledger) = (bucket_id.to_owned(), ledger.clone()); + on_conn!(self, cx, RecordStoreError, |s| s + .put_usage(&bucket_id, window_start, &ledger) + .await) + } + + fn list_metering( + &self, + cx: &mut Op<'_>, + bucket: u64, + ) -> Step>> { + on_conn!(self, cx, RecordStoreError, |s| s + .list_metering(bucket) + .await) + } + + fn purge_windows_before(&self, cx: &mut Op<'_>, before: u64) -> Step> { + on_conn!(self, cx, RecordStoreError, |s| s + .purge_windows_before(before) + .await) + } + + fn purge_metering_before(&self, cx: &mut Op<'_>, bucket: &str) -> Step> { + let bucket = bucket.to_owned(); + on_conn!(self, cx, RecordStoreError, |s| s + .purge_metering_before(&bucket) + .await) + } + + fn put_credential( + &self, + cx: &mut Op<'_>, + secret: &CredentialSecret, + ) -> Step> { + let secret = secret.clone(); + on_conn!(self, cx, RecordStoreError, |s| s + .put_credential(&secret) + .await) + } + + fn put_key_with_credential( + &self, + cx: &mut Op<'_>, + key: &VirtualKey, + secret: &CredentialSecret, + ) -> Step> { + let (key, secret) = (key.clone(), secret.clone()); + on_conn!(self, cx, RecordStoreError, |s| s + .put_key_with_credential(&key, &secret) + .await) + } + + fn list_credentials( + &self, + cx: &mut Op<'_>, + key_id: &str, + ) -> Step>> { + let key_id = key_id.to_owned(); + on_conn!(self, cx, RecordStoreError, |s| s + .list_credentials(&key_id) + .await) + } + + fn lookup_credential_secret( + &self, + cx: &mut Op<'_>, + kind: &str, + public_id: &str, + ) -> Step>> { + let (kind, public_id) = (kind.to_owned(), public_id.to_owned()); + on_conn!(self, cx, RecordStoreError, |s| s + .lookup_credential_secret(&kind, &public_id) + .await) + } + + fn revoke_credential( + &self, + cx: &mut Op<'_>, + id: &str, + reason: &str, + ) -> Step> { + let (id, reason) = (id.to_owned(), reason.to_owned()); + on_conn!(self, cx, RecordStoreError, |s| s + .revoke_credential(&id, &reason) + .await) + } + + fn list_credentials_since( + &self, + cx: &mut Op<'_>, + since: u64, + ) -> Step>> { + on_conn!(self, cx, RecordStoreError, |s| s + .list_credentials_since(since) + .await) + } + + fn list_audit(&self, cx: &mut Op<'_>) -> Step>> { + on_conn!(self, cx, RecordStoreError, |s| s.list_audit().await) + } + + fn add_denylist( + &self, + cx: &mut Op<'_>, + sub: &str, + reason: &str, + ) -> Step> { + let (sub, reason) = (sub.to_owned(), reason.to_owned()); + on_conn!(self, cx, RecordStoreError, |s| s + .add_denylist(&sub, &reason) + .await) + } + + fn list_denylist(&self, cx: &mut Op<'_>) -> Step>> { + on_conn!(self, cx, RecordStoreError, |s| s.list_denylist().await) + } + + fn list_audit_tail( + &self, + cx: &mut Op<'_>, + limit: u64, + ) -> Step>> { + on_conn!(self, cx, RecordStoreError, |s| s + .list_audit_tail(limit) + .await) + } + + fn upsert_plane_record( + &self, + cx: &mut Op<'_>, + record: PlaneRecordRef<'_>, + ) -> Step> { + let record = record.to_record(); + on_conn!(self, cx, RecordStoreError, |s| s + .upsert_plane_record(record.view()) + .await) + } + + fn get_plane_record( + &self, + cx: &mut Op<'_>, + kind: &str, + id: &str, + ) -> Step>>> { + let (kind, id) = (kind.to_owned(), id.to_owned()); + on_conn!(self, cx, RecordStoreError, |s| s + .get_plane_record(&kind, &id) + .await) + } + + fn list_plane_records( + &self, + cx: &mut Op<'_>, + kind: &str, + selector: &PlaneSelector<'_>, + ) -> Step>>> { + let kind = kind.to_owned(); + let selector = selector.to_static(); + on_conn!(self, cx, RecordStoreError, |s| s + .list_plane_records(&kind, &selector) + .await) + } + + fn list_plane_record_parents( + &self, + cx: &mut Op<'_>, + kind: &str, + ) -> Step>> { + let kind = kind.to_owned(); + on_conn!(self, cx, RecordStoreError, |s| s + .list_plane_record_parents(&kind) + .await) + } + + fn purge_plane_records_before( + &self, + cx: &mut Op<'_>, + kind: &str, + before: u64, + ) -> Step> { + let kind = kind.to_owned(); + on_conn!(self, cx, RecordStoreError, |s| s + .purge_plane_records_before(&kind, before) + .await) + } + + fn delete_plane_record( + &self, + cx: &mut Op<'_>, + kind: &str, + id: &str, + ) -> Step> { + let (kind, id) = (kind.to_owned(), id.to_owned()); + on_conn!(self, cx, RecordStoreError, |s| s + .delete_plane_record(&kind, &id) + .await) + } + + fn redeem_plane_token( + &self, + cx: &mut Op<'_>, + kind: &str, + token: &str, + expires_at: u64, + now: u64, + ) -> Step> { + let (kind, token) = (kind.to_owned(), token.to_owned()); + on_conn!(self, cx, RecordStoreError, |s| s + .redeem_plane_token(&kind, &token, expires_at, now) + .await) + } + + fn plane_token_live( + &self, + cx: &mut Op<'_>, + kind: &str, + token: &str, + expires_at: u64, + now: u64, + ) -> Step> { + let (kind, token) = (kind.to_owned(), token.to_owned()); + on_conn!(self, cx, RecordStoreError, |s| s + .plane_token_live(&kind, &token, expires_at, now) + .await) + } +} From e76518fc68cf711df71d23fcbc7e567053fcee3d Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sat, 3 Oct 2026 04:37:45 -0700 Subject: [PATCH 02/16] fleet: busbar pin c7cc4f6696 -> 180d92c205 (lane-dg-storebridge: the root connector wakes a plugin's pending connection and drives dispatcher-worker sockets on its own I/O thread; the real-binary e2e tests need it) --- .busbar-ref | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/consumer-verify.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/repin.yml | 2 +- Cargo.lock | 8 ++++---- Cargo.toml | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.busbar-ref b/.busbar-ref index 7514538..525b8b3 100644 --- a/.busbar-ref +++ b/.busbar-ref @@ -1 +1 @@ -c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8 1.6.0 +180d92c205a7bdc70d1b6de012929757642543c6 1.6.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8acda69..f247fc5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ permissions: jobs: ci: - uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8 + uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@180d92c205a7bdc70d1b6de012929757642543c6 with: service: postgres busbar_checkout: true diff --git a/.github/workflows/consumer-verify.yml b/.github/workflows/consumer-verify.yml index 4d77561..d93bb95 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@c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8 + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@180d92c205a7bdc70d1b6de012929757642543c6 with: version: ${{ inputs.version || '' }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb19ac5..412d23b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: secrets: inherit release: needs: ci - uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8 + uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@180d92c205a7bdc70d1b6de012929757642543c6 with: plugin_crate: busbar-store-postgres-plugin manifest_name: busbar-store-postgres @@ -41,7 +41,7 @@ jobs: consumer-verify: needs: release if: ${{ !cancelled() }} - uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8 + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@180d92c205a7bdc70d1b6de012929757642543c6 with: version: ${{ github.ref_name }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/repin.yml b/.github/workflows/repin.yml index ddb0a78..c3e92e2 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@c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8 + uses: GetBusbar/busbar/.github/workflows/plugin-repin.yml@180d92c205a7bdc70d1b6de012929757642543c6 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 e172ad9..d734793 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,7 +70,7 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "busbar-contract" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8#c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8" +source = "git+https://github.com/GetBusbar/busbar?rev=180d92c205a7bdc70d1b6de012929757642543c6#180d92c205a7bdc70d1b6de012929757642543c6" dependencies = [ "async-trait", "base64", @@ -93,7 +93,7 @@ dependencies = [ [[package]] name = "busbar-kernel-ledger" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8#c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8" +source = "git+https://github.com/GetBusbar/busbar?rev=180d92c205a7bdc70d1b6de012929757642543c6#180d92c205a7bdc70d1b6de012929757642543c6" dependencies = [ "busbar-contract", "ring", @@ -103,7 +103,7 @@ dependencies = [ [[package]] name = "busbar-kernel-wal" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8#c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8" +source = "git+https://github.com/GetBusbar/busbar?rev=180d92c205a7bdc70d1b6de012929757642543c6#180d92c205a7bdc70d1b6de012929757642543c6" dependencies = [ "busbar-contract", "ring", @@ -112,7 +112,7 @@ dependencies = [ [[package]] name = "busbar-plugin-loader" version = "1.5.0" -source = "git+https://github.com/GetBusbar/busbar?rev=c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8#c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8" +source = "git+https://github.com/GetBusbar/busbar?rev=180d92c205a7bdc70d1b6de012929757642543c6#180d92c205a7bdc70d1b6de012929757642543c6" dependencies = [ "busbar-contract", "busbar-kernel-ledger", diff --git a/Cargo.toml b/Cargo.toml index 1654eec..521d443 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,8 +29,8 @@ publish = false repository = "https://github.com/GetBusbar/busbar-store-postgres" [workspace.dependencies] -busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8" } -busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "c7cc4f6696d53654ba1ac4f8e8c4fad0b9cd9df8" } +busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "180d92c205a7bdc70d1b6de012929757642543c6" } +busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "180d92c205a7bdc70d1b6de012929757642543c6" } # busbar's root [workspace.dependencies], third-party rows, at e1d3d8b097201859751d77208e66249d98b50a4b a2a-lf = "0.3.0" a2a-pb = "0.2.0" From 3d6d3de7634e46f240367c79528b5f5c500b0cfc Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sat, 3 Oct 2026 05:21:39 -0700 Subject: [PATCH 03/16] store: keep the instance's connection across ops (1.5.5's one connection); TLS test through the host; busbar pin 180d92c205 -> 8faed15ca4 ARCHITECT rulings 2026-10-03 12:10Z (STORE-KEEP, TLS). - STORE-KEEP: the instance holds the store SDK's kept set (wire::Pool) bounded at KEPT_CONNECTIONS = 1, 1.5.5's one mutex-guarded connection (the 1.5.5 store has no pool setting). The connect step and every op run under wire::drive_kept: a kept connection skips TLS + startup/auth; the connection is kept only if the session is idle (last ReadyForQuery status I, nothing unread, no rollback pending; pgwire::Client::release), and a client dropped any other way (early return, protocol failure, open or failed transaction) discards it, so the next op connects afresh. No retry 1.5.5 did not have. - TLS: unchanged mapping (disable/allow/prefer plaintext as 1.5.5 NoTls; require/verify-* SSLRequest, 'S', upgrade_secure on the DSN host; 'N' fails the load). Docs say the host always verifies. - Tests: src/tests/tls.rs (live): verify_full_secures_the_connection_through_the_host (in-test TLS proxy with a per-run rcgen CA in front of the live server; TcpConns::with_roots), an_untrusted_server_certificate_fails_the_load_in_the_drivers_words, the_store_keeps_one_connection_across_its_ops (one backend pid across the connect step and ten ops). The harness closes each test store's instance on drop. rustls/rustls-pki-types/rcgen are dev-dependencies only. - Pin: busbar 8faed15ca46ec48d5818dc0bd0c22a9c15218de1 (manifests, .busbar-ref, workflows, Cargo.lock). --- .busbar-ref | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/consumer-verify.yml | 2 +- .github/workflows/release.yml | 4 +- .github/workflows/repin.yml | 2 +- Cargo.lock | 246 +++++++++++++++++++++++++- Cargo.toml | 4 +- README.md | 18 +- store-postgres/Cargo.toml | 5 + store-postgres/src/lib.rs | 50 ++++-- store-postgres/src/pgwire.rs | 54 +++++- store-postgres/src/tests.rs | 3 + store-postgres/src/tests/harness.rs | 28 ++- store-postgres/src/tests/tls.rs | 236 ++++++++++++++++++++++++ store-postgres/src/v3.rs | 15 +- 15 files changed, 616 insertions(+), 55 deletions(-) create mode 100644 store-postgres/src/tests/tls.rs diff --git a/.busbar-ref b/.busbar-ref index 525b8b3..beae069 100644 --- a/.busbar-ref +++ b/.busbar-ref @@ -1 +1 @@ -180d92c205a7bdc70d1b6de012929757642543c6 1.6.0 +8faed15ca46ec48d5818dc0bd0c22a9c15218de1 1.6.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f247fc5..8985667 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ permissions: jobs: ci: - uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@180d92c205a7bdc70d1b6de012929757642543c6 + uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@8faed15ca46ec48d5818dc0bd0c22a9c15218de1 with: service: postgres busbar_checkout: true diff --git a/.github/workflows/consumer-verify.yml b/.github/workflows/consumer-verify.yml index d93bb95..bec2531 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@180d92c205a7bdc70d1b6de012929757642543c6 + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@8faed15ca46ec48d5818dc0bd0c22a9c15218de1 with: version: ${{ inputs.version || '' }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 412d23b..831c64d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: secrets: inherit release: needs: ci - uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@180d92c205a7bdc70d1b6de012929757642543c6 + uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@8faed15ca46ec48d5818dc0bd0c22a9c15218de1 with: plugin_crate: busbar-store-postgres-plugin manifest_name: busbar-store-postgres @@ -41,7 +41,7 @@ jobs: consumer-verify: needs: release if: ${{ !cancelled() }} - uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@180d92c205a7bdc70d1b6de012929757642543c6 + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@8faed15ca46ec48d5818dc0bd0c22a9c15218de1 with: version: ${{ github.ref_name }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/repin.yml b/.github/workflows/repin.yml index c3e92e2..2516306 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@180d92c205a7bdc70d1b6de012929757642543c6 + uses: GetBusbar/busbar/.github/workflows/plugin-repin.yml@8faed15ca46ec48d5818dc0bd0c22a9c15218de1 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 d734793..bf9a3f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,45 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "async-trait" version = "0.1.91" @@ -25,6 +64,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "base64" version = "0.22.1" @@ -37,6 +82,15 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.13.1" @@ -70,7 +124,7 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "busbar-contract" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=180d92c205a7bdc70d1b6de012929757642543c6#180d92c205a7bdc70d1b6de012929757642543c6" +source = "git+https://github.com/GetBusbar/busbar?rev=8faed15ca46ec48d5818dc0bd0c22a9c15218de1#8faed15ca46ec48d5818dc0bd0c22a9c15218de1" dependencies = [ "async-trait", "base64", @@ -93,7 +147,7 @@ dependencies = [ [[package]] name = "busbar-kernel-ledger" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=180d92c205a7bdc70d1b6de012929757642543c6#180d92c205a7bdc70d1b6de012929757642543c6" +source = "git+https://github.com/GetBusbar/busbar?rev=8faed15ca46ec48d5818dc0bd0c22a9c15218de1#8faed15ca46ec48d5818dc0bd0c22a9c15218de1" dependencies = [ "busbar-contract", "ring", @@ -103,7 +157,7 @@ dependencies = [ [[package]] name = "busbar-kernel-wal" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=180d92c205a7bdc70d1b6de012929757642543c6#180d92c205a7bdc70d1b6de012929757642543c6" +source = "git+https://github.com/GetBusbar/busbar?rev=8faed15ca46ec48d5818dc0bd0c22a9c15218de1#8faed15ca46ec48d5818dc0bd0c22a9c15218de1" dependencies = [ "busbar-contract", "ring", @@ -112,7 +166,7 @@ dependencies = [ [[package]] name = "busbar-plugin-loader" version = "1.5.0" -source = "git+https://github.com/GetBusbar/busbar?rev=180d92c205a7bdc70d1b6de012929757642543c6#180d92c205a7bdc70d1b6de012929757642543c6" +source = "git+https://github.com/GetBusbar/busbar?rev=8faed15ca46ec48d5818dc0bd0c22a9c15218de1#8faed15ca46ec48d5818dc0bd0c22a9c15218de1" dependencies = [ "busbar-contract", "busbar-kernel-ledger", @@ -124,6 +178,8 @@ dependencies = [ "hex", "libc", "libloading", + "rustls", + "rustls-pki-types", "serde", "serde_json", "tar", @@ -142,6 +198,9 @@ dependencies = [ "postgres", "postgres-protocol", "postgres-types", + "rcgen", + "rustls", + "rustls-pki-types", "serde", "serde_json", ] @@ -304,6 +363,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + [[package]] name = "der" version = "0.7.10" @@ -314,6 +379,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "digest" version = "0.10.7" @@ -827,6 +912,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + [[package]] name = "libc" version = "0.2.189" @@ -895,6 +986,12 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -916,6 +1013,50 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -934,6 +1075,15 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1056,6 +1206,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.107" @@ -1118,7 +1274,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1177,6 +1333,19 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rcgen" +version = "0.14.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8" +dependencies = [ + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1255,6 +1424,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustls" version = "0.23.43" @@ -1558,6 +1736,36 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -2055,6 +2263,34 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + [[package]] name = "yoke" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index 521d443..181f8df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,8 +29,8 @@ publish = false repository = "https://github.com/GetBusbar/busbar-store-postgres" [workspace.dependencies] -busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "180d92c205a7bdc70d1b6de012929757642543c6" } -busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "180d92c205a7bdc70d1b6de012929757642543c6" } +busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "8faed15ca46ec48d5818dc0bd0c22a9c15218de1" } +busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "8faed15ca46ec48d5818dc0bd0c22a9c15218de1" } # busbar's root [workspace.dependencies], third-party rows, at e1d3d8b097201859751d77208e66249d98b50a4b a2a-lf = "0.3.0" a2a-pb = "0.2.0" diff --git a/README.md b/README.md index 2edc954..dc89668 100644 --- a/README.md +++ b/README.md @@ -97,17 +97,17 @@ the engine's JSON config (`{"url": "postgres://..."}`) into a ### Known limitations (documented honestly, not papered over) -- **The store opens no socket of its own.** Every operation reaches - Postgres through busbar's connector (the store declares one outbound - `tcp` need in the `operator-infrastructure` egress class) as its own - connection: connect, authenticate, run, close. A dropped connection - fails that one operation; the next one connects afresh. -- **One connection per operation.** There is no pool yet, so every - operation pays a connect and an authentication. +- **The store opens no socket of its own.** It reaches Postgres through + busbar's connector (the store declares one outbound `tcp` need in the + `operator-infrastructure` egress class). +- **One kept connection, as 1.5.x.** The store connects and authenticates + once and keeps that connection; operations take turns on it. A + connection that fails (or is left inside a transaction) is closed, and + the next operation connects afresh rather than needing a restart. - **TLS through busbar.** `sslmode=require`, `verify-ca` and `verify-full` secure the connection through busbar's connector and its - trust anchors; `disable`, `allow` and `prefer` connect in plaintext, - as 1.5.x (`NoTls`) did. A Unix-socket `host` is refused: the store + trust anchors (always verifying the certificate and name); `disable`, + `allow` and `prefer` connect in plaintext, as 1.5.x (`NoTls`) did. A Unix-socket `host` is refused: the store reaches its server over TCP. See the doc comments at the top of diff --git a/store-postgres/Cargo.toml b/store-postgres/Cargo.toml index a615ff5..cd5838b 100644 --- a/store-postgres/Cargo.toml +++ b/store-postgres/Cargo.toml @@ -36,3 +36,8 @@ serde = { workspace = true } # connection of the `postgres` driver's own. busbar-plugin-loader = { workspace = true, features = ["test-support"] } postgres = { workspace = true } +# The TLS test (`src/tests/tls.rs`): an in-test TLS proxy in front of the live server, its CA minted +# per run; the store's side is the loader's test table trusting that CA. Test-only. +rustls = { workspace = true } +rustls-pki-types = { workspace = true } +rcgen = { version = "0.14", default-features = false, features = ["crypto", "ring"] } diff --git a/store-postgres/src/lib.rs b/store-postgres/src/lib.rs index 954aa23..996c2a3 100644 --- a/store-postgres/src/lib.rs +++ b/store-postgres/src/lib.rs @@ -2,10 +2,10 @@ // Copyright (C) 2026 Busbar Inc and contributors //! The **Postgres** backend for busbar's durable governance store — the shared, multi-node `db` -//! plugin. Serves the store kind's table (`StoreSlots`) over the HOST'S CONNECTOR: every op is one -//! connection, opened, authenticated and closed inside the op as straight-line async code (`pgwire`, -//! the Postgres frontend protocol over the store SDK's `wire`), so the store opens no socket of its -//! own and no op blocks a thread. Depends only on the `busbar-contract` crate (plus the sans-IO +//! plugin. Serves the store kind's table (`StoreSlots`) over the HOST'S CONNECTOR: every op runs on +//! the instance's kept connection as straight-line async code (`pgwire`, the Postgres frontend +//! protocol over the store SDK's `wire`), so the store opens no socket of its own and no op blocks +//! a thread. Depends only on the `busbar-contract` crate (plus the sans-IO //! `postgres-protocol`/`postgres-types` codec), never on the engine. //! //! Served through the store kind's ONE door (`store_door!` in `v3`, the store v3 table of busbar @@ -27,19 +27,24 @@ //! credentials in another would let a `REPEATABLE READ` hydration snapshot land between them and //! observe a "deleted" key whose credential is still live). //! -//! CONNECTIONS. The 1.5.x store held one mutex-guarded connection for its life; this one holds -//! none. Each op dials through the host (its one declared `tcp` need, `operator-infrastructure`), -//! authenticates (SCRAM-SHA-256, md5 or cleartext, as the server asks) and runs its 1.5.5 SQL body -//! on that connection; the schema is ensured once, by `open`'s connect step, so an unreachable or -//! refusing server still fails the load at boot, in the driver's words. +//! CONNECTIONS. The 1.5.x store held one mutex-guarded connection for its life; this one keeps one +//! too (ARCHITECT ruling 2026-10-03, STORE-KEEP: the store SDK's kept set, bounded at +//! [`KEPT_CONNECTIONS`] = 1, so ops take turns on it as they took the mutex). `open`'s connect step +//! dials through the host (its one declared `tcp` need, `operator-infrastructure`), authenticates +//! (SCRAM-SHA-256, md5 or cleartext, as the server asks) and ensures the schema, so an unreachable +//! or refusing server still fails the load at boot, in the driver's words; the connection is then +//! kept, and every op runs its 1.5.5 SQL body on it. A connection is kept only while its session is +//! idle (no transaction open or failed); one that failed or was left mid-transaction is closed, and +//! the next op connects afresh (where 1.5.x needed a restart). //! //! ## Known limitations (documented honestly, not papered over) //! //! - **TLS through the host.** `sslmode=require` / `verify-ca` / `verify-full` asks the server for //! TLS and secures the connection through the host's connector (its trust anchors); `disable`, //! `allow` and `prefer` connect in plaintext, as the 1.5.x build (`NoTls`) did. -//! - **One connection per op.** No pool yet (the host's `checkout`/`checkin` are not offered), so -//! every op pays a connect and an authentication. +//! - **`sslmode=require` verifies.** The host's TLS always verifies the server's certificate and +//! name (libpq's `require` would not); a server that answers the `SSLRequest` with `N` fails the +//! load (`error performing TLS handshake: server does not support TLS`). //! - **No partitioning, no LISTEN/NOTIFY-accelerated hydration, no column-level secret grants in //! this pass.** The design session that produced this schema recommended all three as scale/perf //! layers on top of this contract — deliberately deferred here in favor of getting the @@ -546,14 +551,17 @@ ALTER TABLE usage_metering ADD COLUMN IF NOT EXISTS priced_from_ms BIGINT NOT NU "; /// Postgres `Store` backend (durable, shared across a cluster). The instance holds the parsed -/// connection settings, never a connection: every op is ONE connection over the host's connector -/// (the store SDK's `wire`), opened, authenticated and closed inside the op, so no socket of the -/// store's own exists and no op blocks a thread (busbar THE DESIGN, the connections section and -/// the plugin ABI). The schema is ensured once, by `open`'s connect step. +/// connection settings and its kept connection (the store SDK's `wire::Pool`, one connection, as +/// 1.5.x held one), reached over the host's connector, so no socket of the store's own exists and +/// no op blocks a thread (busbar THE DESIGN, the connections section and the plugin ABI). The +/// schema is ensured once, by `open`'s connect step. pub struct PostgresStore { shared: std::sync::Arc, } +/// How many connections an instance keeps: the 1.5.5 store held exactly one. +pub const KEPT_CONNECTIONS: usize = 1; + /// What every op of one instance shares. pub(crate) struct Shared { /// The connection settings. @@ -562,6 +570,10 @@ pub(crate) struct Shared { pub(crate) secret: Option, /// When this instance last swept `store_ops` past its retention (`v3`). pub(crate) ops_swept_at: AtomicU64, + /// The instance's KEPT connections (ARCHITECT ruling 2026-10-03, STORE-KEEP): bounded at ONE, + /// the 1.5.5 store's one mutex-guarded connection (it had no pool setting). An op waits for it + /// while another holds it, as 1.5.5's ops waited on the mutex. + pub(crate) pool: std::sync::Arc, } /// ONE OP'S CONNECTION: the 1.5.5 bodies run on it, as they ran on the mutex-guarded client. @@ -611,7 +623,7 @@ fn now_secs() -> u64 { } impl PostgresStore { - /// The store on the connection string `conn_str`: parsed here, connected per op. A string + /// The store on the connection string `conn_str`: parsed here, connected by the connect step. A string /// the driver refuses is refused in its words, scrubbed of the DSN password. /// /// # Errors @@ -625,6 +637,7 @@ impl PostgresStore { config, secret, ops_swept_at: AtomicU64::new(0), + pool: busbar_contract::abi::sdk::store::wire::Pool::new(KEPT_CONNECTIONS), }), }) } @@ -648,9 +661,10 @@ impl Session { } } - /// Say goodbye to the server; the op's connection closes when the op answers. + /// The op is done: its connection is kept for the next op if the session is idle, else + /// discarded ([`Client::release`]). pub(crate) async fn close(mut self) { - self.client.terminate().await; + self.client.release(); } fn lock(&mut self) -> &mut Client { diff --git a/store-postgres/src/pgwire.rs b/store-postgres/src/pgwire.rs index 7b1b31b..ac0f270 100644 --- a/store-postgres/src/pgwire.rs +++ b/store-postgres/src/pgwire.rs @@ -534,12 +534,31 @@ impl Row { // ── the client ────────────────────────────────────────────────────────────────────────────── -/// One connection to the server, over the op's wire. +/// One connection to the server, over the op's wire. The connection is KEPT for the instance's +/// next op (the store SDK's kept set) only when [`Client::release`] finds the session idle: the +/// last message read was `ReadyForQuery` with status `I` (no transaction open or failed), nothing +/// is left unread and no transaction awaits its rollback. Dropped any other way (an early return, +/// a protocol failure), it is discarded and the next op dials fresh. pub struct Client { wire: Wire, buf: BytesMut, /// A `Transaction` was dropped uncommitted: roll it back before the next statement. rollback_pending: bool, + /// The transaction status of the last `ReadyForQuery` (`I` idle, `T` in a transaction, `E` in + /// a failed one). + status: u8, + /// The last message read was `ReadyForQuery` and nothing was sent after it. + at_ready: bool, + /// [`Client::release`] found the session idle: keep the connection. + keep: bool, +} + +impl Drop for Client { + fn drop(&mut self) { + if !self.keep { + self.wire.discard(); + } + } } impl fmt::Debug for Client { @@ -561,11 +580,20 @@ impl Client { wire.connect(0, Some(&cfg.target())) .await .map_err(Error::connect)?; + let reused = wire.reused(); let mut c = Client { wire, buf: BytesMut::new(), rollback_pending: false, + status: b'I', + at_ready: true, + keep: false, }; + if reused { + // A kept connection: secured and authenticated by the op that established it, and + // idle when it was kept. + return Ok(c); + } if cfg.ssl == SslMode::Require { let mut out = BytesMut::new(); frontend::ssl_request(&mut out); @@ -679,14 +707,20 @@ impl Client { } } - /// Say goodbye (`Terminate`), best effort: the op's connection closes when it answers. - pub async fn terminate(&mut self) { - let mut out = BytesMut::new(); - frontend::terminate(&mut out); - let _ = self.wire.write_all(&out).await; + /// Whether the session is idle and whole: fit to be kept for the next op. + #[must_use] + pub fn is_idle(&self) -> bool { + self.at_ready && self.status == b'I' && !self.rollback_pending && self.buf.is_empty() + } + + /// The op is done with the connection: keep it if the session is idle, else it is discarded + /// when the client drops. + pub fn release(&mut self) { + self.keep = self.is_idle(); } async fn send(&mut self, bytes: &[u8]) -> Result<(), Error> { + self.at_ready = false; self.wire .write_all(bytes) .await @@ -725,7 +759,13 @@ impl Client { match Message::parse(&mut self.buf).map_err(parse_err)? { Some(Message::ParameterStatus(_) | Message::NoticeResponse(_)) => {} Some(Message::NotificationResponse(_)) => {} - Some(m) => return Ok(m), + Some(m) => { + if let Message::ReadyForQuery(b) = &m { + self.status = b.status(); + self.at_ready = true; + } + return Ok(m); + } None => self.more().await?, } } diff --git a/store-postgres/src/tests.rs b/store-postgres/src/tests.rs index 944a146..9e3afb2 100644 --- a/store-postgres/src/tests.rs +++ b/store-postgres/src/tests.rs @@ -2236,3 +2236,6 @@ mod v160_shapes; // ── THE STORE v3 SLOTS (the door's table beyond the 1.5.5 op set) ────────────────────────────── mod v3_slots; + +// ── TLS through the host (the connector's TLS wrap; ARCHITECT ruling 2026-10-03) ───────────── +mod tls; diff --git a/store-postgres/src/tests/harness.rs b/store-postgres/src/tests/harness.rs index cae1a83..8bee44e 100644 --- a/store-postgres/src/tests/harness.rs +++ b/store-postgres/src/tests/harness.rs @@ -66,9 +66,16 @@ fn mint() -> OpId { /// The store's linked door opened on `settings` over the loader, its needs on a [`TcpConns`]. pub(crate) fn open_loaded(settings: &str) -> Result { + open_loaded_over(settings, |d| Arc::new(TcpConns::new(d.conn_waker()))) +} + +/// [`open_loaded`], its needs on the table `conns` builds (a [`TcpConns`] that trusts a test CA). +pub(crate) fn open_loaded_over( + settings: &str, + conns: impl FnOnce(&Dispatcher) -> Arc, +) -> Result { let d = Arc::new(Dispatcher::new(DispatchConfig::default())); - let conns: Arc = - Arc::new(TcpConns::new(d.conn_waker())); + let conns = conns(&d); let row = LinkedRow::of(crate::door).map_err(|e| e.to_string())?; let p = load_linked::( &row, @@ -91,6 +98,23 @@ pub(crate) struct TestStore { raw: Mutex>, } +/// Close the instance (the loader's `close` slot), as busbar does when it lets a store go: its kept +/// connection closes with it, so a test run never piles connections up on the server. +pub(crate) fn close_instance(store: &LoadedStore) { + use busbar_plugin_loader::dispatch::{in_head, out_head, Frame}; + let mut f = Frame::new(in_head(), out_head()); + let _ = store.plugin().call( + busbar_contract::abi::mechanism::lifecycle::slot::CLOSE, + &mut f, + ); +} + +impl Drop for TestStore { + fn drop(&mut self) { + close_instance(&self.store); + } +} + impl std::ops::Deref for TestStore { type Target = LoadedStore; fn deref(&self) -> &LoadedStore { diff --git a/store-postgres/src/tests/tls.rs b/store-postgres/src/tests/tls.rs new file mode 100644 index 0000000..f1da7ca --- /dev/null +++ b/store-postgres/src/tests/tls.rs @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! TLS THROUGH THE HOST, and the KEPT connection, live (ARCHITECT rulings 2026-10-03: TLS, +//! STORE-KEEP). +//! +//! * `sslmode=verify-full` asks the server for TLS (`SSLRequest`), and on its `S` the store secures +//! the stream through the host (`Wire::upgrade_secure`); here the host is the loader's test table +//! trusting a CA minted for the run, and the server is an in-test TLS proxy in front of the live +//! server (it answers the `SSLRequest`, accepts TLS, and forwards the plaintext). A write and a +//! read round-trip; without the CA the load fails in the driver's words. +//! * The instance keeps ONE connection across its ops (1.5.5's one mutex-guarded connection): every +//! op of a store runs on the same server backend. + +use std::io::{ErrorKind, Read, Write}; +use std::net::{TcpListener, TcpStream}; +use std::sync::Arc; +use std::time::Duration; + +use busbar_contract::records::RecordStore; +use busbar_plugin_loader::tcp_conns::TcpConns; + +use super::harness::{close_instance, open_loaded_over}; +use super::live_url; +use crate::pgwire::Config; + +/// A CA and a `localhost` server config it signed: (ca_der, server config). +fn minted() -> (Vec, Arc) { + let ca_key = rcgen::KeyPair::generate().expect("ca key"); + let mut ca_params = rcgen::CertificateParams::new(Vec::::new()).expect("ca params"); + ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); + let ca = ca_params.self_signed(&ca_key).expect("ca"); + let issuer = rcgen::Issuer::from_params(&ca_params, ca_key); + let key = rcgen::KeyPair::generate().expect("key"); + let leaf = rcgen::CertificateParams::new(vec!["localhost".to_string()]) + .expect("params") + .signed_by(&key, &issuer) + .expect("leaf"); + let server = rustls::ServerConfig::builder_with_provider(Arc::new( + rustls::crypto::ring::default_provider(), + )) + .with_safe_default_protocol_versions() + .expect("versions") + .with_no_client_auth() + .with_single_cert( + vec![leaf.der().clone()], + rustls_pki_types::PrivateKeyDer::Pkcs8(key.serialize_der().into()), + ) + .expect("server config"); + (ca.der().to_vec(), Arc::new(server)) +} + +/// Pump bytes both ways between the TLS side and the server until either closes. +fn pump(mut tls: rustls::StreamOwned, mut up: TcpStream) { + if tls.sock.set_nonblocking(true).is_err() || up.set_nonblocking(true).is_err() { + return; + } + let mut buf = vec![0_u8; 16 * 1024]; + loop { + let mut moved = false; + match tls.read(&mut buf) { + Ok(0) => return, + Ok(n) => { + moved = true; + if write_all(&mut up, &buf[..n]).is_err() { + return; + } + } + Err(e) if e.kind() == ErrorKind::WouldBlock => {} + Err(_) => return, + } + match up.read(&mut buf) { + Ok(0) => return, + Ok(n) => { + moved = true; + if write_all(&mut tls, &buf[..n]).is_err() { + return; + } + } + Err(e) if e.kind() == ErrorKind::WouldBlock => {} + Err(_) => return, + } + if !moved { + std::thread::sleep(Duration::from_millis(1)); + } + } +} + +fn write_all(w: &mut impl Write, mut b: &[u8]) -> std::io::Result<()> { + while !b.is_empty() { + match w.write(b) { + Ok(0) => return Err(ErrorKind::WriteZero.into()), + Ok(n) => b = &b[n..], + Err(e) if e.kind() == ErrorKind::WouldBlock => { + std::thread::sleep(Duration::from_millis(1)); + } + Err(e) => return Err(e), + } + } + loop { + match w.flush() { + Ok(()) => return Ok(()), + Err(e) if e.kind() == ErrorKind::WouldBlock => { + std::thread::sleep(Duration::from_millis(1)); + } + Err(e) => return Err(e), + } + } +} + +/// A TLS proxy in front of `upstream` (`host:port`), speaking Postgres's `SSLRequest` opening: its +/// port. +fn tls_proxy(upstream: String, server: Arc) -> u16 { + let l = TcpListener::bind("127.0.0.1:0").expect("bind"); + let port = l.local_addr().expect("addr").port(); + std::thread::spawn(move || { + for s in l.incoming() { + let Ok(mut s) = s else { return }; + let (server, upstream) = (server.clone(), upstream.clone()); + std::thread::spawn(move || { + // SSLRequest: length 8, code 80877103. + let mut req = [0_u8; 8]; + if s.read_exact(&mut req).is_err() || req != [0, 0, 0, 8, 4, 210, 22, 47] { + return; + } + if s.write_all(b"S").is_err() { + return; + } + let mut conn = rustls::ServerConnection::new(server).expect("tls conn"); + while conn.is_handshaking() { + if conn.complete_io(&mut s).is_err() { + return; + } + } + let Ok(up) = TcpStream::connect(&upstream) else { + return; + }; + pump(rustls::StreamOwned::new(conn, s), up); + }); + } + }); + port +} + +/// The live server's settings with the proxy in front: `postgres://...@localhost:/...?sslmode=verify-full`. +fn through_proxy(url: &str, port: u16) -> (String, String) { + let c = Config::parse(url).expect("the live url parses"); + let upstream = format!("{}:{}", c.host, c.port); + let tls = format!( + "postgres://{}:{}@localhost:{port}/{}?sslmode=verify-full", + c.user, + c.password.clone().unwrap_or_default(), + c.dbname.clone().unwrap_or_default() + ); + (upstream, tls) +} + +/// TLS: `sslmode=verify-full` through the host's TLS (the test table trusting the run's CA) carries +/// the connect step, a write and a read. +#[test] +fn verify_full_secures_the_connection_through_the_host() { + let Some(url) = live_url() else { return }; + let (ca_der, server) = minted(); + let (upstream, _) = through_proxy(&url, 0); + let port = tls_proxy(upstream, server); + let (_, tls_url) = through_proxy(&url, port); + let settings = serde_json::json!({ "url": tls_url }).to_string(); + let store = open_loaded_over(&settings, |d| { + Arc::new(TcpConns::with_roots(d.conn_waker(), &ca_der)) + }) + .expect("the store opens over TLS"); + let sub = format!("tls-{}", std::process::id()); + RecordStore::add_denylist(&store, &sub, "over tls").expect("a write over TLS"); + assert!( + RecordStore::list_denylist(&store) + .expect("a read over TLS") + .contains(&sub), + "the write is read back over TLS" + ); + close_instance(&store); +} + +/// TLS: a host that does not trust the server's certificate refuses the handshake, and the load +/// fails in the driver's words. +#[test] +fn an_untrusted_server_certificate_fails_the_load_in_the_drivers_words() { + let Some(url) = live_url() else { return }; + let (_, server) = minted(); + let (upstream, _) = through_proxy(&url, 0); + let port = tls_proxy(upstream, server); + let (_, tls_url) = through_proxy(&url, port); + let settings = serde_json::json!({ "url": tls_url }).to_string(); + // A table with no trust: every upgrade refused. + let e = open_loaded_over(&settings, |d| Arc::new(TcpConns::new(d.conn_waker()))) + .expect_err("no TLS without trust"); + assert!( + e.contains("open failed: error performing TLS handshake"), + "the driver's words: {e}" + ); +} + +/// STORE-KEEP: every op of one store runs on the same server backend (one kept connection), and +/// the store holds one connection while idle. +#[test] +fn the_store_keeps_one_connection_across_its_ops() { + let Some(url) = live_url() else { return }; + let app = format!("bb_keep_{}", std::process::id()); + let sep = if url.contains('?') { '&' } else { '?' }; + let settings = + serde_json::json!({ "url": format!("{url}{sep}application_name={app}") }).to_string(); + let store = open_loaded_over(&settings, |d| Arc::new(TcpConns::new(d.conn_waker()))) + .expect("the store opens"); + let mut raw = super::connect_client_with_retry(&url); + let backends = |raw: &mut postgres::Client| -> Vec { + raw.query( + "SELECT pid FROM pg_stat_activity WHERE application_name = $1", + &[&app], + ) + .expect("pg_stat_activity") + .iter() + .map(|r| r.get(0)) + .collect() + }; + let after_open = backends(&mut raw); + assert_eq!(after_open.len(), 1, "the connect step's connection is kept"); + for i in 0..5 { + RecordStore::add_denylist(&store, &format!("{app}-{i}"), "keep").expect("a write"); + RecordStore::list_denylist(&store).expect("a read"); + } + assert_eq!( + backends(&mut raw), + after_open, + "every op ran on the one kept backend" + ); + close_instance(&store); +} diff --git a/store-postgres/src/v3.rs b/store-postgres/src/v3.rs index 9410062..3a16886 100644 --- a/store-postgres/src/v3.rs +++ b/store-postgres/src/v3.rs @@ -21,8 +21,9 @@ //! never deadlock), tests each cell with 1.5.5's per-dimension rule (`abi::store::ReserveIn`) and //! applies all or nothing. A grant is valid until `u64::MAX`: nothing in the table expires a slice. //! -//! CONNECTIONS: every slot is ONE connection over the host's connector, run as straight-line async -//! code by the store SDK's `wire::drive` (busbar THE DESIGN: every call Ready or Pending(wake), no +//! CONNECTIONS: every slot runs on the instance's ONE kept connection over the host's connector +//! (1.5.5's one mutex-guarded connection; STORE-KEEP), as straight-line async code by the store +//! SDK's `wire::drive_kept` (busbar THE DESIGN: every call Ready or Pending(wake), no //! socket of the plugin's own; ARCHITECT rulings 2026-10-03 on Q-L14-1 and Q-L16-2). The door //! declares one outbound `tcp` need (`NEEDS`, `operator-infrastructure`); `open` parses the //! settings, and its connect step reaches the server and ensures the schema, so an unreachable or @@ -39,7 +40,7 @@ use busbar_contract::abi::host::conn::connector::{ use busbar_contract::abi::mechanism::call::{AbiStr, Blob, BLOB_OCTETS}; use busbar_contract::abi::sdk::conn::Host; use busbar_contract::abi::sdk::door::abi_str; -use busbar_contract::abi::sdk::store::wire::{drive, Body}; +use busbar_contract::abi::sdk::store::wire::{drive_kept, Body}; use busbar_contract::abi::sdk::store::{ Cap, CapsRefused, Cell, CellKey, Dimension, Grant, Op, OpRefused, OpResult, ReserveRefused, Scanned, Step, StoreSlots, Tail, @@ -92,13 +93,15 @@ busbar_contract::store_door!( needs: NEEDS ); -/// RUN ONE OP over its own connection: open it (a failure is `$fail` of the driver's words), run -/// `$body` on the session `$s`, say goodbye. Every argument the body names is owned (the body runs +/// RUN ONE OP on the instance's kept connection (a fresh one when none is kept): open the session +/// (a failure is `$fail` of the driver's words), run `$body` on it `$s`, and keep the connection if +/// the session is idle. Every argument the body names is owned (the body runs /// across the op's entries). macro_rules! on_conn { ($self:ident, $cx:ident, $fail:expr, |$s:ident| $body:expr) => {{ let shared = $self.shared(); - drive($cx, move |wire| -> Body<_> { + let pool = shared.pool.clone(); + drive_kept($cx, &pool, move |wire| -> Body<_> { Box::pin(async move { let mut $s = match Session::open(wire, shared).await { Ok(s) => s, From 4695727b80c382c5e1f8afaa697565bae06eb670 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sat, 3 Oct 2026 05:41:45 -0700 Subject: [PATCH 04/16] fleet: busbar pin 8faed15ca4 -> 1c9e599003 (close on no ticket; LoadedStore closes its instance on drop) The harness's own close-on-drop workaround is struck: busbar's LoadedStore now closes its instance when dropped, and its kept connection with it. the_store_keeps_one_connection_across_its_ops again asserts that dropping the store closes its backend connection. The full live suite runs at the server's default max_connections (100). --- .busbar-ref | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/consumer-verify.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/repin.yml | 2 +- Cargo.lock | 8 ++++---- Cargo.toml | 4 ++-- store-postgres/src/tests/harness.rs | 17 ----------------- store-postgres/src/tests/tls.rs | 11 ++++++++--- 9 files changed, 20 insertions(+), 32 deletions(-) diff --git a/.busbar-ref b/.busbar-ref index beae069..5670ec4 100644 --- a/.busbar-ref +++ b/.busbar-ref @@ -1 +1 @@ -8faed15ca46ec48d5818dc0bd0c22a9c15218de1 1.6.0 +1c9e599003ad2b2789b271482824b52f390c3200 1.6.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8985667..ddddd02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ permissions: jobs: ci: - uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@8faed15ca46ec48d5818dc0bd0c22a9c15218de1 + uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@1c9e599003ad2b2789b271482824b52f390c3200 with: service: postgres busbar_checkout: true diff --git a/.github/workflows/consumer-verify.yml b/.github/workflows/consumer-verify.yml index bec2531..1786de6 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@8faed15ca46ec48d5818dc0bd0c22a9c15218de1 + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@1c9e599003ad2b2789b271482824b52f390c3200 with: version: ${{ inputs.version || '' }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 831c64d..f228ae7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: secrets: inherit release: needs: ci - uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@8faed15ca46ec48d5818dc0bd0c22a9c15218de1 + uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@1c9e599003ad2b2789b271482824b52f390c3200 with: plugin_crate: busbar-store-postgres-plugin manifest_name: busbar-store-postgres @@ -41,7 +41,7 @@ jobs: consumer-verify: needs: release if: ${{ !cancelled() }} - uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@8faed15ca46ec48d5818dc0bd0c22a9c15218de1 + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@1c9e599003ad2b2789b271482824b52f390c3200 with: version: ${{ github.ref_name }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/repin.yml b/.github/workflows/repin.yml index 2516306..76cab27 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@8faed15ca46ec48d5818dc0bd0c22a9c15218de1 + uses: GetBusbar/busbar/.github/workflows/plugin-repin.yml@1c9e599003ad2b2789b271482824b52f390c3200 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 bf9a3f6..dfda499 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,7 +124,7 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "busbar-contract" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=8faed15ca46ec48d5818dc0bd0c22a9c15218de1#8faed15ca46ec48d5818dc0bd0c22a9c15218de1" +source = "git+https://github.com/GetBusbar/busbar?rev=1c9e599003ad2b2789b271482824b52f390c3200#1c9e599003ad2b2789b271482824b52f390c3200" dependencies = [ "async-trait", "base64", @@ -147,7 +147,7 @@ dependencies = [ [[package]] name = "busbar-kernel-ledger" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=8faed15ca46ec48d5818dc0bd0c22a9c15218de1#8faed15ca46ec48d5818dc0bd0c22a9c15218de1" +source = "git+https://github.com/GetBusbar/busbar?rev=1c9e599003ad2b2789b271482824b52f390c3200#1c9e599003ad2b2789b271482824b52f390c3200" dependencies = [ "busbar-contract", "ring", @@ -157,7 +157,7 @@ dependencies = [ [[package]] name = "busbar-kernel-wal" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=8faed15ca46ec48d5818dc0bd0c22a9c15218de1#8faed15ca46ec48d5818dc0bd0c22a9c15218de1" +source = "git+https://github.com/GetBusbar/busbar?rev=1c9e599003ad2b2789b271482824b52f390c3200#1c9e599003ad2b2789b271482824b52f390c3200" dependencies = [ "busbar-contract", "ring", @@ -166,7 +166,7 @@ dependencies = [ [[package]] name = "busbar-plugin-loader" version = "1.5.0" -source = "git+https://github.com/GetBusbar/busbar?rev=8faed15ca46ec48d5818dc0bd0c22a9c15218de1#8faed15ca46ec48d5818dc0bd0c22a9c15218de1" +source = "git+https://github.com/GetBusbar/busbar?rev=1c9e599003ad2b2789b271482824b52f390c3200#1c9e599003ad2b2789b271482824b52f390c3200" dependencies = [ "busbar-contract", "busbar-kernel-ledger", diff --git a/Cargo.toml b/Cargo.toml index 181f8df..f4c9279 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,8 +29,8 @@ publish = false repository = "https://github.com/GetBusbar/busbar-store-postgres" [workspace.dependencies] -busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "8faed15ca46ec48d5818dc0bd0c22a9c15218de1" } -busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "8faed15ca46ec48d5818dc0bd0c22a9c15218de1" } +busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "1c9e599003ad2b2789b271482824b52f390c3200" } +busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "1c9e599003ad2b2789b271482824b52f390c3200" } # busbar's root [workspace.dependencies], third-party rows, at e1d3d8b097201859751d77208e66249d98b50a4b a2a-lf = "0.3.0" a2a-pb = "0.2.0" diff --git a/store-postgres/src/tests/harness.rs b/store-postgres/src/tests/harness.rs index 8bee44e..cb4b8da 100644 --- a/store-postgres/src/tests/harness.rs +++ b/store-postgres/src/tests/harness.rs @@ -98,23 +98,6 @@ pub(crate) struct TestStore { raw: Mutex>, } -/// Close the instance (the loader's `close` slot), as busbar does when it lets a store go: its kept -/// connection closes with it, so a test run never piles connections up on the server. -pub(crate) fn close_instance(store: &LoadedStore) { - use busbar_plugin_loader::dispatch::{in_head, out_head, Frame}; - let mut f = Frame::new(in_head(), out_head()); - let _ = store.plugin().call( - busbar_contract::abi::mechanism::lifecycle::slot::CLOSE, - &mut f, - ); -} - -impl Drop for TestStore { - fn drop(&mut self) { - close_instance(&self.store); - } -} - impl std::ops::Deref for TestStore { type Target = LoadedStore; fn deref(&self) -> &LoadedStore { diff --git a/store-postgres/src/tests/tls.rs b/store-postgres/src/tests/tls.rs index f1da7ca..d34d2e6 100644 --- a/store-postgres/src/tests/tls.rs +++ b/store-postgres/src/tests/tls.rs @@ -20,7 +20,7 @@ use std::time::Duration; use busbar_contract::records::RecordStore; use busbar_plugin_loader::tcp_conns::TcpConns; -use super::harness::{close_instance, open_loaded_over}; +use super::harness::open_loaded_over; use super::live_url; use crate::pgwire::Config; @@ -177,7 +177,6 @@ fn verify_full_secures_the_connection_through_the_host() { .contains(&sub), "the write is read back over TLS" ); - close_instance(&store); } /// TLS: a host that does not trust the server's certificate refuses the handshake, and the load @@ -232,5 +231,11 @@ fn the_store_keeps_one_connection_across_its_ops() { after_open, "every op ran on the one kept backend" ); - close_instance(&store); + // Dropping the store closes its instance, and its kept connection with it. + drop(store); + let gone = (0..50).any(|_| { + std::thread::sleep(Duration::from_millis(20)); + backends(&mut raw).is_empty() + }); + assert!(gone, "closing the instance closes its kept connection"); } From 8e98e96b73cee55e715ffb99422f94c54e8832b1 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:24:01 -0700 Subject: [PATCH 05/16] conformance: the RED arms are their own tests beside the both-ways test The fleet's bothways gate needs a RED arm in the conformance target as a test of its own. The foreign-bytes arm and the as-another-kind arm move out of the_linked_and_the_dropped_in_postgres_store_are_one_store into foreign_bytes_dropped_in_are_not_the_postgres_store and the_postgres_store_library_loaded_as_another_kind_is_refused, every assertion kept; neither needs a database. The loader at the pin dlopens the path it is given (no memfd), and the foreign image sits under its own directory, so the arm no longer depends on running first. --- store-postgres-plugin/tests/conformance.rs | 54 +++++++++++++--------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/store-postgres-plugin/tests/conformance.rs b/store-postgres-plugin/tests/conformance.rs index 77bf39a..562d8e8 100644 --- a/store-postgres-plugin/tests/conformance.rs +++ b/store-postgres-plugin/tests/conformance.rs @@ -17,7 +17,7 @@ //! tests use: unset under CI is a FAILURE, unset locally skips only this scenario), one durable //! scenario per door — upsert, point read, a child chain, single-use token redemption, delete. //! -//! RED ARMS, in the same test and always run (no database needed): the library asked for as +//! RED ARMS, each its own test and always run (no database needed): the library asked for as //! `kind: secret` is refused before any slot is called, and DIFFERENT bytes (the cdylib with its //! object magic broken) are not the store — so the comparison above cannot pass vacuously. @@ -137,30 +137,11 @@ fn transcript( }) } -/// The Postgres store behaves as ONE store through either door — and the RED arms show the +/// The Postgres store behaves as ONE store through either door; the RED arms below show the /// comparison is not vacuous. #[test] fn the_linked_and_the_dropped_in_postgres_store_are_one_store() { let live = live_url(); - let lib = common::cdylib(); - - // RED ARM 1 — RUN FIRST: DIFFERENT bytes (the object's magic broken) are not the store. It must - // run before ANY good image is loaded from a path the loader may have mapped already. - let mut foreign = std::fs::read(&lib).expect("read the cdylib"); - foreign[..4].copy_from_slice(b"XXXX"); - let dir = std::env::temp_dir().join(format!("store-postgres-conf-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - let broken = dir.join(lib.file_name().unwrap()); - std::fs::write(&broken, &foreign).unwrap(); - let red = match common::dropped_at(&broken, "{}") { - Ok(_) => panic!("foreign bytes opened as the store"), - Err(e) => e, - }; - assert!( - !red.contains("requires a \"url\""), - "foreign bytes cannot speak the store's own refusal: {red}" - ); let linked = transcript("linked", &common::linked, live.as_deref()); let dropped_in = transcript("dropped", &common::dropped, live.as_deref()); @@ -197,8 +178,36 @@ fn the_linked_and_the_dropped_in_postgres_store_are_one_store() { }) ); } +} - // RED ARM 2: the store's library asked for as another kind is refused before any slot runs. +/// RED: DIFFERENT bytes (the object's magic broken) are not the store. The foreign image is +/// written under its own directory, so the loader `dlopen`s a path no good image was ever loaded +/// from, whatever order the tests in this target run in. +#[test] +fn foreign_bytes_dropped_in_are_not_the_postgres_store() { + let lib = common::cdylib(); + let mut foreign = std::fs::read(&lib).expect("read the cdylib"); + foreign[..4].copy_from_slice(b"XXXX"); + let dir = std::env::temp_dir().join(format!("store-postgres-conf-red-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let broken = dir.join(lib.file_name().unwrap()); + std::fs::write(&broken, &foreign).unwrap(); + let red = match common::dropped_at(&broken, "{}") { + Ok(_) => panic!("foreign bytes opened as the store"), + Err(e) => e, + }; + let _ = std::fs::remove_dir_all(&dir); + assert!( + !red.contains("requires a \"url\""), + "foreign bytes cannot speak the store's own refusal: {red}" + ); +} + +/// RED: the store's library asked for as another kind is refused before any slot runs. +#[test] +fn the_postgres_store_library_loaded_as_another_kind_is_refused() { + let lib = common::cdylib(); let d = Arc::new(Dispatcher::new(DispatchConfig::default())); let bind = Bind { instance: Arc::from("store-postgres-as-secret"), @@ -211,5 +220,4 @@ fn the_linked_and_the_dropped_in_postgres_store_are_one_store() { load_dropped::(&lib, &common::stated(), bind).is_err(), "a store library loaded as kind secret" ); - let _ = std::fs::remove_dir_all(&dir); } From 9f597fba35a2f305bb00e565bdfa39d80e17ef7a Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:24:01 -0700 Subject: [PATCH 06/16] Cargo.lock: re-derived from busbar's lock at the pin (1c9e599003) Seeded from busbar's Cargo.lock at the pin and re-resolved: every crate both locks hold is at busbar's version (rustls 0.23.43 -> 0.23.45 clears RUSTSEC-2026-0285), except the sha2 0.11 line postgres-protocol 0.6.12 needs (sha2 0.11.0, digest 0.11.3, block-buffer 0.12.1, crypto-common 0.2.2, const-oid 0.10.2) and wasi 0.14.7 (dev-only: postgres -> tokio-postgres 0.7.18 -> whoami 2). Older postgres-protocol (<= 0.6.10, sha2 0.10) carries RUSTSEC-2026-0179/0180 and tokio-postgres < 0.7.18 RUSTSEC-2026-0178. --- Cargo.lock | 486 +++++++++++++++++------------------------------------ 1 file changed, 153 insertions(+), 333 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dfda499..d47130d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,7 +32,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.117", "synstructure", ] @@ -44,14 +44,14 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.117", ] [[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", @@ -93,9 +93,9 @@ dependencies = [ [[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" @@ -232,9 +232,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", @@ -248,15 +248,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", @@ -301,9 +301,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", ] @@ -360,14 +360,14 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.117", ] [[package]] name = "data-encoding" -version = "2.11.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "der" @@ -423,13 +423,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.7" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 2.0.117", ] [[package]] @@ -492,9 +492,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", @@ -511,9 +511,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -526,9 +526,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -536,15 +536,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -553,38 +553,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "futures-sink" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -669,9 +669,9 @@ dependencies = [ [[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", @@ -689,9 +689,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", @@ -708,18 +708,18 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hybrid-array" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" 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", @@ -774,88 +774,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" @@ -869,12 +787,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 = "279259b0ac81c89d11c290495fdcfa96ea3643b7df311c138b6fe8ca5237f0f8" +dependencies = [ + "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 = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +checksum = "11c13906586a4b339310541a274dd927aff6fcbb5b8e3af90634c4b31681c792" dependencies = [ - "icu_normalizer", - "icu_properties", + "unicode-joining-type", ] [[package]] @@ -903,20 +831,21 @@ 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", ] [[package]] name = "lazy_static" -version = "1.5.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" @@ -936,19 +865,13 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" dependencies = [ "libc", ] -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - [[package]] name = "lock_api" version = "0.4.14" @@ -960,9 +883,9 @@ dependencies = [ [[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" @@ -982,9 +905,9 @@ dependencies = [ [[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 = "minimal-lexical" @@ -994,9 +917,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[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", @@ -1004,9 +927,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 0.11.1+wasi-snapshot-preview1", @@ -1025,9 +948,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.8" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", @@ -1041,9 +964,9 @@ checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" -version = "0.1.47" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] @@ -1197,15 +1120,6 @@ dependencies = [ "postgres-protocol", ] -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -1214,18 +1128,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[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", @@ -1265,23 +1179,23 @@ 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", "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[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", ] @@ -1411,9 +1325,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" @@ -1435,9 +1349,9 @@ dependencies = [ [[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", @@ -1459,9 +1373,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", @@ -1470,9 +1384,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" @@ -1584,9 +1498,9 @@ dependencies = [ [[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" @@ -1605,9 +1519,9 @@ checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "siphasher" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" [[package]] name = "slab" @@ -1617,15 +1531,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", @@ -1641,12 +1555,6 @@ dependencies = [ "der", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - [[package]] name = "stringprep" version = "0.1.5" @@ -1666,9 +1574,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", @@ -1703,7 +1611,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.117", ] [[package]] @@ -1718,29 +1626,29 @@ dependencies = [ [[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", + "syn 2.0.117", ] [[package]] name = "time" -version = "0.3.55" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" dependencies = [ "deranged", "num-conv", @@ -1758,29 +1666,19 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.32" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" dependencies = [ "num-conv", "time-core", ] -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - [[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", ] @@ -1833,9 +1731,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", @@ -1843,14 +1741,13 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.19" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", "futures-sink", - "libc", "pin-project-lite", "tokio", ] @@ -1919,7 +1816,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.117", ] [[package]] @@ -1967,6 +1864,12 @@ 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" @@ -2050,9 +1953,9 @@ dependencies = [ [[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", ] @@ -2068,9 +1971,9 @@ dependencies = [ [[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", @@ -2081,9 +1984,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", @@ -2091,9 +1994,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", @@ -2101,31 +2004,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", @@ -2143,18 +2046,18 @@ 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", ] [[package]] name = "whoami" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" dependencies = [ "libc", "libredox", @@ -2257,12 +2160,6 @@ 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 = "x509-parser" version = "0.18.1" @@ -2291,91 +2188,14 @@ dependencies = [ "time", ] -[[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" From 07d7369d66024785fbabad1ac80e500de8fed8de Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 5 Oct 2026 04:53:33 -0700 Subject: [PATCH 07/16] fleet: busbar pin 1c9e599003 -> d85c166728 (the parity gate's declared lines); Cargo.lock re-derived from busbar's lock at the pin Seeded from busbar's Cargo.lock at d85c166728 and re-resolved: every crate both locks hold is at busbar's version except the lines busbar's .github/fleet/deps.toml [parity-lines] declares for this repo (sha2 0.11, digest 0.11, block-buffer 0.12, crypto-common 0.2, const-oid 0.10 shipped through postgres-protocol 0.6.12; wasi 0.14 tests-only through the postgres test client). xtask plugin-gates parity at the pin: 0 findings. --- .busbar-ref | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/consumer-verify.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/repin.yml | 2 +- Cargo.lock | 25 +++++++++++++++++++------ Cargo.toml | 4 ++-- 7 files changed, 27 insertions(+), 14 deletions(-) diff --git a/.busbar-ref b/.busbar-ref index 5670ec4..3d84b0d 100644 --- a/.busbar-ref +++ b/.busbar-ref @@ -1 +1 @@ -1c9e599003ad2b2789b271482824b52f390c3200 1.6.0 +d85c166728f0bc17b68477248c8d86cf648187ad 1.6.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddddd02..d21cab7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ permissions: jobs: ci: - uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@1c9e599003ad2b2789b271482824b52f390c3200 + uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@d85c166728f0bc17b68477248c8d86cf648187ad with: service: postgres busbar_checkout: true diff --git a/.github/workflows/consumer-verify.yml b/.github/workflows/consumer-verify.yml index 1786de6..c73700e 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@1c9e599003ad2b2789b271482824b52f390c3200 + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@d85c166728f0bc17b68477248c8d86cf648187ad with: version: ${{ inputs.version || '' }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f228ae7..9b42f4e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: secrets: inherit release: needs: ci - uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@1c9e599003ad2b2789b271482824b52f390c3200 + uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@d85c166728f0bc17b68477248c8d86cf648187ad with: plugin_crate: busbar-store-postgres-plugin manifest_name: busbar-store-postgres @@ -41,7 +41,7 @@ jobs: consumer-verify: needs: release if: ${{ !cancelled() }} - uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@1c9e599003ad2b2789b271482824b52f390c3200 + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@d85c166728f0bc17b68477248c8d86cf648187ad with: version: ${{ github.ref_name }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/repin.yml b/.github/workflows/repin.yml index 76cab27..66a4042 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@1c9e599003ad2b2789b271482824b52f390c3200 + uses: GetBusbar/busbar/.github/workflows/plugin-repin.yml@d85c166728f0bc17b68477248c8d86cf648187ad 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 d47130d..0ebaa2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,7 +124,7 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "busbar-contract" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=1c9e599003ad2b2789b271482824b52f390c3200#1c9e599003ad2b2789b271482824b52f390c3200" +source = "git+https://github.com/GetBusbar/busbar?rev=d85c166728f0bc17b68477248c8d86cf648187ad#d85c166728f0bc17b68477248c8d86cf648187ad" dependencies = [ "async-trait", "base64", @@ -147,7 +147,7 @@ dependencies = [ [[package]] name = "busbar-kernel-ledger" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=1c9e599003ad2b2789b271482824b52f390c3200#1c9e599003ad2b2789b271482824b52f390c3200" +source = "git+https://github.com/GetBusbar/busbar?rev=d85c166728f0bc17b68477248c8d86cf648187ad#d85c166728f0bc17b68477248c8d86cf648187ad" dependencies = [ "busbar-contract", "ring", @@ -157,7 +157,7 @@ dependencies = [ [[package]] name = "busbar-kernel-wal" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=1c9e599003ad2b2789b271482824b52f390c3200#1c9e599003ad2b2789b271482824b52f390c3200" +source = "git+https://github.com/GetBusbar/busbar?rev=d85c166728f0bc17b68477248c8d86cf648187ad#d85c166728f0bc17b68477248c8d86cf648187ad" dependencies = [ "busbar-contract", "ring", @@ -166,7 +166,7 @@ dependencies = [ [[package]] name = "busbar-plugin-loader" version = "1.5.0" -source = "git+https://github.com/GetBusbar/busbar?rev=1c9e599003ad2b2789b271482824b52f390c3200#1c9e599003ad2b2789b271482824b52f390c3200" +source = "git+https://github.com/GetBusbar/busbar?rev=d85c166728f0bc17b68477248c8d86cf648187ad#d85c166728f0bc17b68477248c8d86cf648187ad" dependencies = [ "busbar-contract", "busbar-kernel-ledger", @@ -178,8 +178,6 @@ dependencies = [ "hex", "libc", "libloading", - "rustls", - "rustls-pki-types", "serde", "serde_json", "tar", @@ -1299,12 +1297,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots", ] @@ -2024,6 +2024,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.99" diff --git a/Cargo.toml b/Cargo.toml index f4c9279..18aba7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,8 +29,8 @@ publish = false repository = "https://github.com/GetBusbar/busbar-store-postgres" [workspace.dependencies] -busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "1c9e599003ad2b2789b271482824b52f390c3200" } -busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "1c9e599003ad2b2789b271482824b52f390c3200" } +busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "d85c166728f0bc17b68477248c8d86cf648187ad" } +busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "d85c166728f0bc17b68477248c8d86cf648187ad" } # busbar's root [workspace.dependencies], third-party rows, at e1d3d8b097201859751d77208e66249d98b50a4b a2a-lf = "0.3.0" a2a-pb = "0.2.0" From f0af1006f90b2ed22e70a89cb3213f172fe27248 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 5 Oct 2026 04:56:44 -0700 Subject: [PATCH 08/16] fleet: Cargo.toml and deny.toml rendered at busbar d85c166728 (busbar-release #161: the [parity-lines] rows open their lines) The workspace table and deny.toml are the render of busbar-release PR #161 (p4-parity-lines, f20a4dc) at pin d85c166728: busbar's dependency policy at the pin, and the declared sha2 0.11, digest 0.11, block-buffer 0.12, crypto-common 0.2, const-oid 0.10 and wasi 0.14 lines allowed. rcgen now comes from the workspace table (its row carries the features the TLS test asked for). --- Cargo.toml | 27 ++++++++++++++------------- deny.toml | 34 +++++++++++++--------------------- store-postgres/Cargo.toml | 2 +- 3 files changed, 28 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 18aba7a..4b1d0f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,8 @@ # `.busbar-ref`; no sibling-checkout path dependency ships in any manifest. # # THIRD-PARTY VERSIONS ARE THE FLEET'S, NOT THIS REPO'S. The table below is busbar's own root -# `[workspace.dependencies]` (third-party rows) at busbar e1d3d8b097201859751d77208e66249d98b50a4b, followed by the plugin-only rows -# of busbar's `.github/fleet/deps.toml` at 5489d5f61537027db7c4abf2a049a4c6233735c0. A member takes a crate with +# `[workspace.dependencies]` (third-party rows) at busbar d85c166728f0bc17b68477248c8d86cf648187ad, followed by the plugin-only rows +# of busbar's `.github/fleet/deps.toml` at d85c166728f0bc17b68477248c8d86cf648187ad. A member takes a crate with # `{ workspace = true }` (adding features if it needs them); a crate in neither table is a new # `deps.toml` row in busbar first. CI holds this repo's Cargo.lock to busbar's lock at the pin. [workspace] @@ -31,13 +31,13 @@ repository = "https://github.com/GetBusbar/busbar-store-postgres" [workspace.dependencies] busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "d85c166728f0bc17b68477248c8d86cf648187ad" } busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "d85c166728f0bc17b68477248c8d86cf648187ad" } -# busbar's root [workspace.dependencies], third-party rows, at e1d3d8b097201859751d77208e66249d98b50a4b +# busbar's root [workspace.dependencies], third-party rows, at d85c166728f0bc17b68477248c8d86cf648187ad a2a-lf = "0.3.0" a2a-pb = "0.2.0" arc-swap = "1" async-trait = "0.1" axum = "0.8" -base64 = "0.23" +base64 = "0.22" bumpalo = "3" bytes = "1" core_affinity = "0.8" @@ -50,16 +50,15 @@ futures = "0.3" getrandom = "0.3" h2 = "0.4" hex = "0.4.3" -hmac = "0.13.0" http = "1" http-body = "1" http-body-util = "0.1" httpdate = "1.0" hyper = { version = "1", default-features = false, features = ["server", "client", "http1", "http2"] } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "ring", "webpki-tokio"] } hyper-util = { version = "0.1", default-features = false, features = ["client-legacy", "http1", "http2", "server-auto", "server-graceful", "service", "tokio"] } +idna_adapter = "=1.1.0" indexmap = { version = "2", features = ["serde"] } -jsonschema = "0.49" +jsonschema = { version = "0.49", default-features = false } libc = "0.2" libloading = "0.9" log = "0.4" @@ -68,12 +67,12 @@ memchr = "2" metrics = "0.24.6" metrics-exporter-prometheus = { version = "0.18.3", default-features = false } metrics-util = { version = "0.20.4", default-features = false } -oauth-as = { version = "1.0.0", features = ["http", "jwt", "consent", "resource-metadata"] } +oauth-as = { version = "1.0.0", features = ["http", "jwt", "consent", "resource-metadata", "par", "dpop", "client-assertion"] } opentelemetry-proto = { version = "0.32", default-features = false, features = ["gen-tonic-messages", "trace"] } proc-macro2 = { version = "1", features = ["span-locations"] } proptest = "1" prost = { version = "0.14", default-features = false, features = ["std"] } -rcgen = "0.14" +rcgen = { version = "0.14", default-features = false, features = ["crypto", "ring"] } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream"] } ring = "0.17" rmcp = { version = "3.1.2", default-features = false } @@ -84,12 +83,12 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" serde_urlencoded = "0.7" serde_yaml = { package = "serde_yaml_ng", version = "0.10" } -sha2 = "0.11.0" +sha2 = "0.10" smallvec = "1" socket2 = { version = "0.6", features = ["all"] } sonic-rs = "0.5" syn = { version = "2", features = ["full", "visit", "parsing", "printing"] } -tar = "0.4" +tar = { version = "0.4", default-features = false } tikv-jemalloc-ctl = "0.6" tikv-jemallocator = "0.6" tokio = "1" @@ -108,9 +107,11 @@ url = "2" webpki = { package = "rustls-webpki", version = "0.103", default-features = false, features = ["std", "ring"] } webpki-roots = "1" zeroize = "1" -# busbar .github/fleet/deps.toml [plugin-deps], at 5489d5f61537027db7c4abf2a049a4c6233735c0 +# busbar .github/fleet/deps.toml [plugin-deps], at d85c166728f0bc17b68477248c8d86cf648187ad ldap3 = { version = "0.12", default-features = false, features = ["sync"] } -mysql = { version = "26", default-features = false, features = ["minimal"] } +mysql = { version = "28", default-features = false, features = ["minimal-rust"] } +mysql_common = { version = "0.35", default-features = false } +percent-encoding = "2" postgres = "0.19" redis = { version = "1", default-features = false, features = ["script"] } rusqlite = { version = "0.40", features = ["bundled"] } diff --git a/deny.toml b/deny.toml index 44e1617..5ddbdde 100644 --- a/deny.toml +++ b/deny.toml @@ -1,5 +1,5 @@ # RENDERED by `busbar-release plugin sync` from GetBusbar/busbar-release template/ and busbar's -# `.github/fleet/deps.toml` at 5489d5f61537027db7c4abf2a049a4c6233735c0; a hand edit is overwritten by the next sync. CI runs +# `.github/fleet/deps.toml` at d85c166728f0bc17b68477248c8d86cf648187ad; a hand edit is overwritten by the next sync. CI runs # `cargo deny check` on it (busbar's plugin-ci.yml at the pin). The license allowlist is busbar's own; # the one reviewed git source is busbar itself, at the pin. [graph] @@ -88,8 +88,6 @@ deny = [ { crate = "h2:>=0.5.0", reason = "busbar resolves h2 to 0.4.18; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "hex:<0.4.0", reason = "busbar resolves hex to 0.4.3; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "hex:>=0.5.0", reason = "busbar resolves hex to 0.4.3; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "hmac:<0.13.0", reason = "busbar resolves hmac to 0.13.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "hmac:>=0.14.0", reason = "busbar resolves hmac to 0.13.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "http:<1.0.0", reason = "busbar resolves http to 1.4.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "http:>=2.0.0", reason = "busbar resolves http to 1.4.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "http-body:<1.0.0", reason = "busbar resolves http-body to 1.1.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, @@ -100,10 +98,10 @@ deny = [ { crate = "httpdate:>=2.0.0", reason = "busbar resolves httpdate to 1.0.3; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "hyper:<1.0.0", reason = "busbar resolves hyper to 1.11.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "hyper:>=2.0.0", reason = "busbar resolves hyper to 1.11.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "hyper-rustls:<0.27.0", reason = "busbar resolves hyper-rustls to 0.27.9; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "hyper-rustls:>=0.28.0", reason = "busbar resolves hyper-rustls to 0.27.9; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "hyper-util:<0.1.0", reason = "busbar resolves hyper-util to 0.1.20; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "hyper-util:>=0.2.0", reason = "busbar resolves hyper-util to 0.1.20; a plugin may not pull a second version (OWNER 2026-10-02)" }, + { crate = "idna_adapter:<1.0.0", reason = "busbar resolves idna_adapter to 1.1.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, + { crate = "idna_adapter:>=2.0.0", reason = "busbar resolves idna_adapter to 1.1.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "indexmap:<2.0.0", reason = "busbar resolves indexmap to 2.14.2; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "indexmap:>=3.0.0", reason = "busbar resolves indexmap to 2.14.2; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "jsonschema:<0.49.0", reason = "busbar resolves jsonschema to 0.49.9; a plugin may not pull a second version (OWNER 2026-10-02)" }, @@ -136,8 +134,8 @@ deny = [ { crate = "prost:>=0.15.0", reason = "busbar resolves prost to 0.14.3; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "rcgen:<0.14.0", reason = "busbar resolves rcgen to 0.14.10; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "rcgen:>=0.15.0", reason = "busbar resolves rcgen to 0.14.10; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "reqwest:<0.12.0", reason = "busbar resolves reqwest to 0.12.28, 0.13.4; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "reqwest:>=0.14.0", reason = "busbar resolves reqwest to 0.12.28, 0.13.4; a plugin may not pull a second version (OWNER 2026-10-02)" }, + { crate = "reqwest:<0.12.0", reason = "busbar resolves reqwest to 0.12.28; a plugin may not pull a second version (OWNER 2026-10-02)" }, + { crate = "reqwest:>=0.13.0", reason = "busbar resolves reqwest to 0.12.28; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "ring:<0.17.0", reason = "busbar resolves ring to 0.17.14; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "ring:>=0.18.0", reason = "busbar resolves ring to 0.17.14; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "rmcp:<3.0.0", reason = "busbar resolves rmcp to 3.1.2; a plugin may not pull a second version (OWNER 2026-10-02)" }, @@ -156,8 +154,8 @@ deny = [ { crate = "serde_urlencoded:>=0.8.0", reason = "busbar resolves serde_urlencoded to 0.7.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "serde_yaml_ng:<0.10.0", reason = "busbar resolves serde_yaml_ng to 0.10.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "serde_yaml_ng:>=0.11.0", reason = "busbar resolves serde_yaml_ng to 0.10.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "sha2:<0.10.0", reason = "busbar resolves sha2 to 0.10.9, 0.11.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "sha2:>=0.12.0", reason = "busbar resolves sha2 to 0.10.9, 0.11.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, + { crate = "sha2:<0.10.0", reason = "busbar resolves sha2 to 0.10.9; a plugin may not pull a second version (OWNER 2026-10-02)" }, + { crate = "sha2:>=0.12.0", reason = "busbar resolves sha2 to 0.10.9; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "smallvec:<1.0.0", reason = "busbar resolves smallvec to 1.16.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "smallvec:>=2.0.0", reason = "busbar resolves smallvec to 1.16.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "socket2:<0.6.0", reason = "busbar resolves socket2 to 0.6.4; a plugin may not pull a second version (OWNER 2026-10-02)" }, @@ -212,16 +210,8 @@ skip = [ { crate = "base64@0.23.1", reason = "busbar's own lock holds this duplicate" }, { crate = "bit-vec@0.8.0", reason = "busbar's own lock holds this duplicate" }, { crate = "bit-vec@0.9.1", reason = "busbar's own lock holds this duplicate" }, - { crate = "block-buffer@0.10.4", reason = "busbar's own lock holds this duplicate" }, - { crate = "block-buffer@0.12.0", reason = "busbar's own lock holds this duplicate" }, - { crate = "const-oid@0.10.2", reason = "busbar's own lock holds this duplicate" }, - { crate = "const-oid@0.9.6", reason = "busbar's own lock holds this duplicate" }, { crate = "cpufeatures@0.2.17", reason = "busbar's own lock holds this duplicate" }, { crate = "cpufeatures@0.3.0", reason = "busbar's own lock holds this duplicate" }, - { crate = "crypto-common@0.1.7", reason = "busbar's own lock holds this duplicate" }, - { crate = "crypto-common@0.2.2", reason = "busbar's own lock holds this duplicate" }, - { crate = "digest@0.10.7", reason = "busbar's own lock holds this duplicate" }, - { crate = "digest@0.11.3", reason = "busbar's own lock holds this duplicate" }, { crate = "foldhash@0.1.5", reason = "busbar's own lock holds this duplicate" }, { crate = "foldhash@0.2.0", reason = "busbar's own lock holds this duplicate" }, { crate = "getrandom@0.2.17", reason = "busbar's own lock holds this duplicate" }, @@ -241,14 +231,16 @@ skip = [ { crate = "rand_core@0.10.1", reason = "busbar's own lock holds this duplicate" }, { crate = "rand_core@0.6.4", reason = "busbar's own lock holds this duplicate" }, { crate = "rand_core@0.9.5", reason = "busbar's own lock holds this duplicate" }, - { crate = "reqwest@0.12.28", reason = "busbar's own lock holds this duplicate" }, - { crate = "reqwest@0.13.4", reason = "busbar's own lock holds this duplicate" }, - { crate = "sha2@0.10.9", reason = "busbar's own lock holds this duplicate" }, - { crate = "sha2@0.11.0", reason = "busbar's own lock holds this duplicate" }, { crate = "syn@2.0.117", reason = "busbar's own lock holds this duplicate" }, { crate = "syn@3.0.3", reason = "busbar's own lock holds this duplicate" }, { crate = "windows-sys@0.52.0", reason = "busbar's own lock holds this duplicate" }, { crate = "windows-sys@0.61.2", reason = "busbar's own lock holds this duplicate" }, + { crate = "sha2:>=0.11.0, <0.12.0", reason = "the 0.11 line of sha2 is declared for this repo in busbar's .github/fleet/deps.toml [parity-lines]" }, + { crate = "digest:>=0.11.0, <0.12.0", reason = "the 0.11 line of digest is declared for this repo in busbar's .github/fleet/deps.toml [parity-lines]" }, + { crate = "block-buffer:>=0.12.0, <0.13.0", reason = "the 0.12 line of block-buffer is declared for this repo in busbar's .github/fleet/deps.toml [parity-lines]" }, + { crate = "crypto-common:>=0.2.0, <0.3.0", reason = "the 0.2 line of crypto-common is declared for this repo in busbar's .github/fleet/deps.toml [parity-lines]" }, + { crate = "const-oid:>=0.10.0, <0.11.0", reason = "the 0.10 line of const-oid is declared for this repo in busbar's .github/fleet/deps.toml [parity-lines]" }, + { crate = "wasi:>=0.14.0, <0.15.0", reason = "the 0.14 line of wasi is declared for this repo in busbar's .github/fleet/deps.toml [parity-lines]" }, ] [sources] diff --git a/store-postgres/Cargo.toml b/store-postgres/Cargo.toml index cd5838b..7e7b799 100644 --- a/store-postgres/Cargo.toml +++ b/store-postgres/Cargo.toml @@ -40,4 +40,4 @@ postgres = { workspace = true } # per run; the store's side is the loader's test table trusting that CA. Test-only. rustls = { workspace = true } rustls-pki-types = { workspace = true } -rcgen = { version = "0.14", default-features = false, features = ["crypto", "ring"] } +rcgen = { workspace = true } From 78e4dfa046191f41cc659b16f09ad182326a8c29 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 5 Oct 2026 05:01:18 -0700 Subject: [PATCH 09/16] conformance: adopt busbar's published suite (conformance_suite!, the store script over the live Postgres) store-postgres-plugin/tests/conformance.rs runs busbar_plugin_loader::conformance_suite! over the logic crate's door and the built cdylib, with conformance.json naming the CI service (postgres://busbar:busbar@localhost:5432/busbar_test) and the store section's inputs. The repo's own both-ways test and its two RED tests stay beside it. busbar-plugin-loader gains the `conformance` feature as a dev-dependency. The suite's both-ways arm and two of its RED arms do not pass against this store at d85c166728: the store script pins one crossing per op, but every op of a store that talks to its server over the connector pends on its reads and is resumed (measured 2 to 26 crossings per op); red_ready expects `open` to answer READY in one call while this store connects in `open`; the script's credentials are kind "generic", which the store's schema refuses (kind IN ('sigv4')); and both legs (and the parallel RED fold) share one database under the same ids and op ids. --- store-postgres-plugin/Cargo.toml | 2 +- store-postgres-plugin/tests/conformance.json | 12 ++++++++++++ store-postgres-plugin/tests/conformance.rs | 9 +++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 store-postgres-plugin/tests/conformance.json diff --git a/store-postgres-plugin/Cargo.toml b/store-postgres-plugin/Cargo.toml index 728266a..45aeb01 100644 --- a/store-postgres-plugin/Cargo.toml +++ b/store-postgres-plugin/Cargo.toml @@ -28,7 +28,7 @@ busbar-contract = { workspace = true } # The conformance and end-to-end tests load the built cdylib over the REAL loader (sign, pack, scan, # open) and the linked row through the same registry — 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 = { workspace = true, features = ["test-support"] } +busbar-plugin-loader = { workspace = true, features = ["test-support", "conformance"] } busbar-contract = { workspace = true } serde_json = { workspace = true } postgres = { workspace = true } diff --git a/store-postgres-plugin/tests/conformance.json b/store-postgres-plugin/tests/conformance.json new file mode 100644 index 0000000..3d2f9c4 --- /dev/null +++ b/store-postgres-plugin/tests/conformance.json @@ -0,0 +1,12 @@ +{ + "settings": { "url": "postgres://busbar:busbar@localhost:5432/busbar_test" }, + "store": { + "node": 7, + "caps": { "bucket": "conf-cap", "window_start": 1700000000000, "requests": 3, "input": 100 }, + "draw": { "requests": 2, "input": 60 }, + "usage": { "bucket": "conf-usage", "window": 1700000000 }, + "stream": "conf-stream", + "schema": "conf-schema", + "keys": { "grouped": "vk_conf_grouped", "group": "conf-group", "plain": "vk_conf_plain", "absent": "vk_conf_absent" } + } +} diff --git a/store-postgres-plugin/tests/conformance.rs b/store-postgres-plugin/tests/conformance.rs index 562d8e8..ff234a2 100644 --- a/store-postgres-plugin/tests/conformance.rs +++ b/store-postgres-plugin/tests/conformance.rs @@ -23,6 +23,15 @@ mod common; +// THE PUBLISHED SUITE (busbar-plugin-loader's `conformance` feature, at the pin): the linked door and +// the built cdylib, each through the one loader, driven by the store kind's script over the live +// Postgres `conformance.json` names; exact crossing counts, the two folds equal, its RED arms. +busbar_plugin_loader::conformance_suite! { + door: busbar_store_postgres::door, + cdylib: "busbar_store_postgres_plugin", + inputs: include_str!("conformance.json"), +} + use busbar_contract::records::{PlaneDisposition, PlaneRecord, PlaneSelector, RecordStore}; use busbar_plugin_loader::dispatch::kinds::secret::Secret; use busbar_plugin_loader::dispatch::{load_dropped, Bind, DispatchConfig, Dispatcher, NoSink}; From 4ebd5061241a379d7f6c7958b0c50449c66bd2c1 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 5 Oct 2026 05:07:55 -0700 Subject: [PATCH 10/16] tests/tls.rs: the host's TLS is a SecureDial the test hands the table (busbar d85c166728 TcpConns::with_tls) At the pin the loader's test table names no TLS library: TcpConns::with_roots is gone and a test hands it the TLS it secures with. The TLS test's is rustls trusting the run's CA alone (certificate and name verified), the handshake the old with_roots ran, moved into the test. --- store-postgres/src/tests/tls.rs | 71 ++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/store-postgres/src/tests/tls.rs b/store-postgres/src/tests/tls.rs index d34d2e6..8845439 100644 --- a/store-postgres/src/tests/tls.rs +++ b/store-postgres/src/tests/tls.rs @@ -18,12 +18,79 @@ use std::sync::Arc; use std::time::Duration; use busbar_contract::records::RecordStore; -use busbar_plugin_loader::tcp_conns::TcpConns; +use busbar_plugin_loader::tcp_conns::{SecureDial, SecuredSock, TcpConns}; use super::harness::open_loaded_over; use super::live_url; use crate::pgwire::Config; +/// The host's TLS for the test table ([`TcpConns::with_tls`]): rustls trusting `ca_der` alone, +/// verifying the certificate and the name; the handshake runs to completion on the blocking socket +/// the table hands it. +struct Trusting(Arc); + +fn trusting(ca_der: &[u8]) -> Arc { + let mut roots = rustls::RootCertStore::empty(); + roots + .add(rustls_pki_types::CertificateDer::from(ca_der.to_vec())) + .expect("the test CA is a root certificate"); + let config = rustls::ClientConfig::builder_with_provider(Arc::new( + rustls::crypto::ring::default_provider(), + )) + .with_safe_default_protocol_versions() + .expect("the default protocol versions") + .with_root_certificates(roots) + .with_no_client_auth(); + Arc::new(Trusting(Arc::new(config))) +} + +impl SecureDial for Trusting { + fn secure( + &self, + server: &str, + _verify_off: bool, + mut tcp: TcpStream, + ) -> std::io::Result> { + let name = rustls_pki_types::ServerName::try_from(server.to_owned()) + .map_err(|e| std::io::Error::new(ErrorKind::InvalidInput, e))?; + let mut tls = rustls::ClientConnection::new(Arc::clone(&self.0), name) + .map_err(|e| std::io::Error::new(ErrorKind::InvalidData, e))?; + while tls.is_handshaking() { + tls.complete_io(&mut tcp)?; + } + Ok(Box::new(Secured(rustls::StreamOwned::new(tls, tcp)))) + } +} + +/// A stream [`Trusting`] secured. +struct Secured(rustls::StreamOwned); + +impl Read for Secured { + fn read(&mut self, b: &mut [u8]) -> std::io::Result { + self.0.read(b) + } +} + +impl Write for Secured { + fn write(&mut self, b: &[u8]) -> std::io::Result { + self.0.write(b) + } + fn flush(&mut self) -> std::io::Result<()> { + self.0.flush() + } +} + +impl SecuredSock for Secured { + fn tcp(&self) -> &TcpStream { + self.0.get_ref() + } + fn close(&mut self) { + self.0.conn.send_close_notify(); + let _ = self.0.flush(); + let _ = self.0.sock.shutdown(std::net::Shutdown::Both); + } +} + /// A CA and a `localhost` server config it signed: (ca_der, server config). fn minted() -> (Vec, Arc) { let ca_key = rcgen::KeyPair::generate().expect("ca key"); @@ -166,7 +233,7 @@ fn verify_full_secures_the_connection_through_the_host() { let (_, tls_url) = through_proxy(&url, port); let settings = serde_json::json!({ "url": tls_url }).to_string(); let store = open_loaded_over(&settings, |d| { - Arc::new(TcpConns::with_roots(d.conn_waker(), &ca_der)) + Arc::new(TcpConns::with_tls(d.conn_waker(), trusting(&ca_der))) }) .expect("the store opens over TLS"); let sub = format!("tls-{}", std::process::id()); From 8f463f7ccd6407cf1bcb45205d44bdb54e84d85a Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 5 Oct 2026 05:24:51 -0700 Subject: [PATCH 11/16] admin_api_e2e: boot #1 states its memory store (busbar d85c166728 requires a store: block) At the pin busbar refuses a config without `store:` (BUSBAR-9007), so the first boot's config now names `store: {module: memory}`, the RAM store the absent block used to mean. --- store-postgres-plugin/tests/admin_api_e2e.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/store-postgres-plugin/tests/admin_api_e2e.rs b/store-postgres-plugin/tests/admin_api_e2e.rs index 043ef39..ef8a673 100644 --- a/store-postgres-plugin/tests/admin_api_e2e.rs +++ b/store-postgres-plugin/tests/admin_api_e2e.rs @@ -6,8 +6,8 @@ //! real and still kept as its own proof), but the REAL admin HTTP API: //! //! 1. Boot a real `busbar` process (no `--validate`) with its admin listener up and the compiled-in -//! `memory` store active (`store:` block absent — see `busbar`'s own `StoreCfg` doc: "Absent -//! block = the compiled-in ephemeral RAM store"). +//! `memory` store active (`store: {module: memory}`: busbar 1.6.0 requires the block and names +//! the RAM store this way). //! 2. `POST /api/v1/admin/plugins` with the REAL built cdylib, base64-encoded, guarded by a real //! `x-admin-token` — the exact wire shape `crates/busbar/src/admin/v1/json/handlers.rs`'s //! `install_plugin` and its own admin test (`test_admin_v1_plugin_install_list_reload_remove` @@ -433,11 +433,15 @@ fn install_over_admin_api_then_mint_a_key_and_verify_postgres_directly() { plugins_dir.display() ); - // BOOT #1: no `store:` block at all -- the compiled-in `memory` store, per StoreCfg's own doc - // ("Absent block = the compiled-in ephemeral RAM store"). The postgres plugin is NOT on disk - // yet: this process only exists to serve the admin API that installs it. + // BOOT #1: the compiled-in `memory` store (`store: {module: memory}`; busbar 1.6.0 refuses a + // config without a `store:` block, BUSBAR-9007). The postgres plugin is NOT on disk yet: this + // process only exists to serve the admin API that installs it. let config1 = work.join("config1.yaml"); - std::fs::write(&config1, &providers_and_common).unwrap(); + std::fs::write( + &config1, + format!("{providers_and_common}store:\n module: memory\n"), + ) + .unwrap(); let mut guard1 = spawn_busbar( Command::new(&busbar_bin) From a92e91584e2d7ac3e57df1591c183fa474e8d985 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 5 Oct 2026 05:29:07 -0700 Subject: [PATCH 12/16] fleet: busbar pin d85c166728 -> 16ee8eaa39 (the published suite's per-fold namespace and resumes); Cargo.lock re-derived from busbar's lock at the pin; Cargo.toml and deny.toml the busbar-release #161 render at the pin xtask plugin-gates parity at the pin: 0 findings; cargo deny: clean. --- .busbar-ref | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/consumer-verify.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/repin.yml | 2 +- Cargo.lock | 8 ++++---- Cargo.toml | 12 ++++++------ deny.toml | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.busbar-ref b/.busbar-ref index 3d84b0d..cc165cb 100644 --- a/.busbar-ref +++ b/.busbar-ref @@ -1 +1 @@ -d85c166728f0bc17b68477248c8d86cf648187ad 1.6.0 +16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 1.6.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d21cab7..52b0e19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ permissions: jobs: ci: - uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@d85c166728f0bc17b68477248c8d86cf648187ad + uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 with: service: postgres busbar_checkout: true diff --git a/.github/workflows/consumer-verify.yml b/.github/workflows/consumer-verify.yml index c73700e..83ae1bc 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@d85c166728f0bc17b68477248c8d86cf648187ad + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 with: version: ${{ inputs.version || '' }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9b42f4e..78a6eb6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: secrets: inherit release: needs: ci - uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@d85c166728f0bc17b68477248c8d86cf648187ad + uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 with: plugin_crate: busbar-store-postgres-plugin manifest_name: busbar-store-postgres @@ -41,7 +41,7 @@ jobs: consumer-verify: needs: release if: ${{ !cancelled() }} - uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@d85c166728f0bc17b68477248c8d86cf648187ad + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 with: version: ${{ github.ref_name }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/repin.yml b/.github/workflows/repin.yml index 66a4042..59c3217 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@d85c166728f0bc17b68477248c8d86cf648187ad + uses: GetBusbar/busbar/.github/workflows/plugin-repin.yml@16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 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 0ebaa2a..9302aab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,7 +124,7 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "busbar-contract" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=d85c166728f0bc17b68477248c8d86cf648187ad#d85c166728f0bc17b68477248c8d86cf648187ad" +source = "git+https://github.com/GetBusbar/busbar?rev=16ee8eaa39ca992fafa7e3311b03d6d60d718bd6#16ee8eaa39ca992fafa7e3311b03d6d60d718bd6" dependencies = [ "async-trait", "base64", @@ -147,7 +147,7 @@ dependencies = [ [[package]] name = "busbar-kernel-ledger" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=d85c166728f0bc17b68477248c8d86cf648187ad#d85c166728f0bc17b68477248c8d86cf648187ad" +source = "git+https://github.com/GetBusbar/busbar?rev=16ee8eaa39ca992fafa7e3311b03d6d60d718bd6#16ee8eaa39ca992fafa7e3311b03d6d60d718bd6" dependencies = [ "busbar-contract", "ring", @@ -157,7 +157,7 @@ dependencies = [ [[package]] name = "busbar-kernel-wal" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=d85c166728f0bc17b68477248c8d86cf648187ad#d85c166728f0bc17b68477248c8d86cf648187ad" +source = "git+https://github.com/GetBusbar/busbar?rev=16ee8eaa39ca992fafa7e3311b03d6d60d718bd6#16ee8eaa39ca992fafa7e3311b03d6d60d718bd6" dependencies = [ "busbar-contract", "ring", @@ -166,7 +166,7 @@ dependencies = [ [[package]] name = "busbar-plugin-loader" version = "1.5.0" -source = "git+https://github.com/GetBusbar/busbar?rev=d85c166728f0bc17b68477248c8d86cf648187ad#d85c166728f0bc17b68477248c8d86cf648187ad" +source = "git+https://github.com/GetBusbar/busbar?rev=16ee8eaa39ca992fafa7e3311b03d6d60d718bd6#16ee8eaa39ca992fafa7e3311b03d6d60d718bd6" dependencies = [ "busbar-contract", "busbar-kernel-ledger", diff --git a/Cargo.toml b/Cargo.toml index 4b1d0f5..78f51c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,8 @@ # `.busbar-ref`; no sibling-checkout path dependency ships in any manifest. # # THIRD-PARTY VERSIONS ARE THE FLEET'S, NOT THIS REPO'S. The table below is busbar's own root -# `[workspace.dependencies]` (third-party rows) at busbar d85c166728f0bc17b68477248c8d86cf648187ad, followed by the plugin-only rows -# of busbar's `.github/fleet/deps.toml` at d85c166728f0bc17b68477248c8d86cf648187ad. A member takes a crate with +# `[workspace.dependencies]` (third-party rows) at busbar 16ee8eaa39ca992fafa7e3311b03d6d60d718bd6, followed by the plugin-only rows +# of busbar's `.github/fleet/deps.toml` at 16ee8eaa39ca992fafa7e3311b03d6d60d718bd6. A member takes a crate with # `{ workspace = true }` (adding features if it needs them); a crate in neither table is a new # `deps.toml` row in busbar first. CI holds this repo's Cargo.lock to busbar's lock at the pin. [workspace] @@ -29,9 +29,9 @@ publish = false repository = "https://github.com/GetBusbar/busbar-store-postgres" [workspace.dependencies] -busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "d85c166728f0bc17b68477248c8d86cf648187ad" } -busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "d85c166728f0bc17b68477248c8d86cf648187ad" } -# busbar's root [workspace.dependencies], third-party rows, at d85c166728f0bc17b68477248c8d86cf648187ad +busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "16ee8eaa39ca992fafa7e3311b03d6d60d718bd6" } +busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "16ee8eaa39ca992fafa7e3311b03d6d60d718bd6" } +# busbar's root [workspace.dependencies], third-party rows, at 16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 a2a-lf = "0.3.0" a2a-pb = "0.2.0" arc-swap = "1" @@ -107,7 +107,7 @@ url = "2" webpki = { package = "rustls-webpki", version = "0.103", default-features = false, features = ["std", "ring"] } webpki-roots = "1" zeroize = "1" -# busbar .github/fleet/deps.toml [plugin-deps], at d85c166728f0bc17b68477248c8d86cf648187ad +# busbar .github/fleet/deps.toml [plugin-deps], at 16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 ldap3 = { version = "0.12", default-features = false, features = ["sync"] } mysql = { version = "28", default-features = false, features = ["minimal-rust"] } mysql_common = { version = "0.35", default-features = false } diff --git a/deny.toml b/deny.toml index 5ddbdde..129f7ee 100644 --- a/deny.toml +++ b/deny.toml @@ -1,5 +1,5 @@ # RENDERED by `busbar-release plugin sync` from GetBusbar/busbar-release template/ and busbar's -# `.github/fleet/deps.toml` at d85c166728f0bc17b68477248c8d86cf648187ad; a hand edit is overwritten by the next sync. CI runs +# `.github/fleet/deps.toml` at 16ee8eaa39ca992fafa7e3311b03d6d60d718bd6; a hand edit is overwritten by the next sync. CI runs # `cargo deny check` on it (busbar's plugin-ci.yml at the pin). The license allowlist is busbar's own; # the one reviewed git source is busbar itself, at the pin. [graph] From 0e58c23806fc10878604a9e5a7876a5c6888214f Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 5 Oct 2026 05:29:43 -0700 Subject: [PATCH 13/16] tests: binds name their connection table (busbar 16ee8eaa39 ConnTable) At the pin a Bind's table is a ConnTable: the live tests' binds are ConnTable::Host over the loader's test table; the as-another-kind RED test binds a Probe (no table, nothing opened), so its refusal is still the kind's. --- store-postgres-plugin/tests/common/mod.rs | 5 +++-- store-postgres-plugin/tests/conformance.rs | 6 ++++-- store-postgres/src/tests/harness.rs | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/store-postgres-plugin/tests/common/mod.rs b/store-postgres-plugin/tests/common/mod.rs index d5cb445..404b780 100644 --- a/store-postgres-plugin/tests/common/mod.rs +++ b/store-postgres-plugin/tests/common/mod.rs @@ -14,7 +14,8 @@ use std::sync::Arc; use busbar_plugin_loader::dispatch::kinds::store::Store; use busbar_plugin_loader::dispatch::{ - load_dropped, load_linked, rendering_of, Bind, DispatchConfig, Dispatcher, LinkedRow, NoSink, + load_dropped, load_linked, rendering_of, Bind, ConnTable, DispatchConfig, Dispatcher, + LinkedRow, NoSink, }; use busbar_plugin_loader::store_v3::LoadedStore; use busbar_plugin_loader::tcp_conns::TcpConns; @@ -70,7 +71,7 @@ fn bind(d: &Dispatcher) -> Bind { max_inflight_cap: 64, sink: Arc::new(NoSink), dispatcher: d.adopter(), - conns: Some(Arc::new(TcpConns::new(d.conn_waker()))), + conns: ConnTable::Host(Arc::new(TcpConns::new(d.conn_waker()))), } } diff --git a/store-postgres-plugin/tests/conformance.rs b/store-postgres-plugin/tests/conformance.rs index ff234a2..0b1d0a4 100644 --- a/store-postgres-plugin/tests/conformance.rs +++ b/store-postgres-plugin/tests/conformance.rs @@ -34,7 +34,9 @@ busbar_plugin_loader::conformance_suite! { use busbar_contract::records::{PlaneDisposition, PlaneRecord, PlaneSelector, RecordStore}; use busbar_plugin_loader::dispatch::kinds::secret::Secret; -use busbar_plugin_loader::dispatch::{load_dropped, Bind, DispatchConfig, Dispatcher, NoSink}; +use busbar_plugin_loader::dispatch::{ + load_dropped, Bind, ConnTable, DispatchConfig, Dispatcher, NoSink, +}; use busbar_plugin_loader::store_v3::LoadedStore; use std::sync::Arc; @@ -223,7 +225,7 @@ fn the_postgres_store_library_loaded_as_another_kind_is_refused() { max_inflight_cap: 64, sink: Arc::new(NoSink), dispatcher: d.adopter(), - conns: None, + conns: ConnTable::Probe, }; assert!( load_dropped::(&lib, &common::stated(), bind).is_err(), diff --git a/store-postgres/src/tests/harness.rs b/store-postgres/src/tests/harness.rs index cb4b8da..15b68c6 100644 --- a/store-postgres/src/tests/harness.rs +++ b/store-postgres/src/tests/harness.rs @@ -21,7 +21,7 @@ use busbar_contract::records::{AuditRecord, UsageDelta}; use busbar_contract::store_calls::{StoreCalls, StoreFailure}; use busbar_plugin_loader::dispatch::kinds::store::Store; use busbar_plugin_loader::dispatch::{ - load_linked, Bind, DispatchConfig, Dispatcher, LinkedRow, NoSink, + load_linked, Bind, ConnTable, DispatchConfig, Dispatcher, LinkedRow, NoSink, }; use busbar_plugin_loader::store_v3::LoadedStore; use busbar_plugin_loader::tcp_conns::TcpConns; @@ -84,7 +84,7 @@ pub(crate) fn open_loaded_over( max_inflight_cap: 64, sink: Arc::new(NoSink), dispatcher: d.adopter(), - conns: Some(conns), + conns: ConnTable::Host(conns), }, ) .map_err(|e| e.to_string())?; From a86b6d9410b00fc0b530eb050fc92471f4d8aa20 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 5 Oct 2026 05:41:03 -0700 Subject: [PATCH 14/16] fleet: busbar pin 16ee8eaa39 -> c4ed953148 (the published suite's per-fold namespace hooks); Cargo.lock re-derived from busbar's lock at the pin; Cargo.toml and deny.toml the busbar-release #161 render at the pin xtask plugin-gates parity at the pin: 0 findings; cargo deny: clean. --- .busbar-ref | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/consumer-verify.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/repin.yml | 2 +- Cargo.lock | 8 ++++---- Cargo.toml | 12 ++++++------ deny.toml | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.busbar-ref b/.busbar-ref index cc165cb..407b821 100644 --- a/.busbar-ref +++ b/.busbar-ref @@ -1 +1 @@ -16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 1.6.0 +c4ed9531489031942edc1ca5e43377904bc73f80 1.6.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52b0e19..8714d5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ permissions: jobs: ci: - uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 + uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@c4ed9531489031942edc1ca5e43377904bc73f80 with: service: postgres busbar_checkout: true diff --git a/.github/workflows/consumer-verify.yml b/.github/workflows/consumer-verify.yml index 83ae1bc..d34c6c4 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@16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@c4ed9531489031942edc1ca5e43377904bc73f80 with: version: ${{ inputs.version || '' }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78a6eb6..61e86d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: secrets: inherit release: needs: ci - uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 + uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@c4ed9531489031942edc1ca5e43377904bc73f80 with: plugin_crate: busbar-store-postgres-plugin manifest_name: busbar-store-postgres @@ -41,7 +41,7 @@ jobs: consumer-verify: needs: release if: ${{ !cancelled() }} - uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@c4ed9531489031942edc1ca5e43377904bc73f80 with: version: ${{ github.ref_name }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/repin.yml b/.github/workflows/repin.yml index 59c3217..0517e51 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@16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 + uses: GetBusbar/busbar/.github/workflows/plugin-repin.yml@c4ed9531489031942edc1ca5e43377904bc73f80 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 9302aab..a62cc82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,7 +124,7 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "busbar-contract" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=16ee8eaa39ca992fafa7e3311b03d6d60d718bd6#16ee8eaa39ca992fafa7e3311b03d6d60d718bd6" +source = "git+https://github.com/GetBusbar/busbar?rev=c4ed9531489031942edc1ca5e43377904bc73f80#c4ed9531489031942edc1ca5e43377904bc73f80" dependencies = [ "async-trait", "base64", @@ -147,7 +147,7 @@ dependencies = [ [[package]] name = "busbar-kernel-ledger" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=16ee8eaa39ca992fafa7e3311b03d6d60d718bd6#16ee8eaa39ca992fafa7e3311b03d6d60d718bd6" +source = "git+https://github.com/GetBusbar/busbar?rev=c4ed9531489031942edc1ca5e43377904bc73f80#c4ed9531489031942edc1ca5e43377904bc73f80" dependencies = [ "busbar-contract", "ring", @@ -157,7 +157,7 @@ dependencies = [ [[package]] name = "busbar-kernel-wal" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=16ee8eaa39ca992fafa7e3311b03d6d60d718bd6#16ee8eaa39ca992fafa7e3311b03d6d60d718bd6" +source = "git+https://github.com/GetBusbar/busbar?rev=c4ed9531489031942edc1ca5e43377904bc73f80#c4ed9531489031942edc1ca5e43377904bc73f80" dependencies = [ "busbar-contract", "ring", @@ -166,7 +166,7 @@ dependencies = [ [[package]] name = "busbar-plugin-loader" version = "1.5.0" -source = "git+https://github.com/GetBusbar/busbar?rev=16ee8eaa39ca992fafa7e3311b03d6d60d718bd6#16ee8eaa39ca992fafa7e3311b03d6d60d718bd6" +source = "git+https://github.com/GetBusbar/busbar?rev=c4ed9531489031942edc1ca5e43377904bc73f80#c4ed9531489031942edc1ca5e43377904bc73f80" dependencies = [ "busbar-contract", "busbar-kernel-ledger", diff --git a/Cargo.toml b/Cargo.toml index 78f51c4..5542683 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,8 @@ # `.busbar-ref`; no sibling-checkout path dependency ships in any manifest. # # THIRD-PARTY VERSIONS ARE THE FLEET'S, NOT THIS REPO'S. The table below is busbar's own root -# `[workspace.dependencies]` (third-party rows) at busbar 16ee8eaa39ca992fafa7e3311b03d6d60d718bd6, followed by the plugin-only rows -# of busbar's `.github/fleet/deps.toml` at 16ee8eaa39ca992fafa7e3311b03d6d60d718bd6. A member takes a crate with +# `[workspace.dependencies]` (third-party rows) at busbar c4ed9531489031942edc1ca5e43377904bc73f80, followed by the plugin-only rows +# of busbar's `.github/fleet/deps.toml` at c4ed9531489031942edc1ca5e43377904bc73f80. A member takes a crate with # `{ workspace = true }` (adding features if it needs them); a crate in neither table is a new # `deps.toml` row in busbar first. CI holds this repo's Cargo.lock to busbar's lock at the pin. [workspace] @@ -29,9 +29,9 @@ publish = false repository = "https://github.com/GetBusbar/busbar-store-postgres" [workspace.dependencies] -busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "16ee8eaa39ca992fafa7e3311b03d6d60d718bd6" } -busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "16ee8eaa39ca992fafa7e3311b03d6d60d718bd6" } -# busbar's root [workspace.dependencies], third-party rows, at 16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 +busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "c4ed9531489031942edc1ca5e43377904bc73f80" } +busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "c4ed9531489031942edc1ca5e43377904bc73f80" } +# busbar's root [workspace.dependencies], third-party rows, at c4ed9531489031942edc1ca5e43377904bc73f80 a2a-lf = "0.3.0" a2a-pb = "0.2.0" arc-swap = "1" @@ -107,7 +107,7 @@ url = "2" webpki = { package = "rustls-webpki", version = "0.103", default-features = false, features = ["std", "ring"] } webpki-roots = "1" zeroize = "1" -# busbar .github/fleet/deps.toml [plugin-deps], at 16ee8eaa39ca992fafa7e3311b03d6d60d718bd6 +# busbar .github/fleet/deps.toml [plugin-deps], at c4ed9531489031942edc1ca5e43377904bc73f80 ldap3 = { version = "0.12", default-features = false, features = ["sync"] } mysql = { version = "28", default-features = false, features = ["minimal-rust"] } mysql_common = { version = "0.35", default-features = false } diff --git a/deny.toml b/deny.toml index 129f7ee..b05cf61 100644 --- a/deny.toml +++ b/deny.toml @@ -1,5 +1,5 @@ # RENDERED by `busbar-release plugin sync` from GetBusbar/busbar-release template/ and busbar's -# `.github/fleet/deps.toml` at 16ee8eaa39ca992fafa7e3311b03d6d60d718bd6; a hand edit is overwritten by the next sync. CI runs +# `.github/fleet/deps.toml` at c4ed9531489031942edc1ca5e43377904bc73f80; a hand edit is overwritten by the next sync. CI runs # `cargo deny check` on it (busbar's plugin-ci.yml at the pin). The license allowlist is busbar's own; # the one reviewed git source is busbar itself, at the pin. [graph] From f6bdce40af147134d7be9b3c59f55d180bf182ed Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 5 Oct 2026 05:42:23 -0700 Subject: [PATCH 15/16] conformance: each suite fold opens over a schema of its own (namespace hooks, search_path={fold}), credential kind sigv4 conformance.json's url sets search_path to the fold's namespace; the suite's namespace hooks create that schema before each fold and drop it (CASCADE) after, on an independent connection of the postgres driver, so the two legs and CI's debug, release and RED runs never share rows. The store's credential kind input is sigv4, the one its schema holds. Locally (Postgres 17): the suite's six tests and the repo's three pass in release and debug, the moved count fails on the comparator, and no fold schema is left behind. --- store-postgres-plugin/tests/conformance.json | 3 +- store-postgres-plugin/tests/conformance.rs | 31 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/store-postgres-plugin/tests/conformance.json b/store-postgres-plugin/tests/conformance.json index 3d2f9c4..d23e15c 100644 --- a/store-postgres-plugin/tests/conformance.json +++ b/store-postgres-plugin/tests/conformance.json @@ -1,6 +1,7 @@ { - "settings": { "url": "postgres://busbar:busbar@localhost:5432/busbar_test" }, + "settings": { "url": "postgres://busbar:busbar@localhost:5432/busbar_test?options=-c%20search_path%3D{fold}" }, "store": { + "credential_kind": "sigv4", "node": 7, "caps": { "bucket": "conf-cap", "window_start": 1700000000000, "requests": 3, "input": 100 }, "draw": { "requests": 2, "input": 60 }, diff --git a/store-postgres-plugin/tests/conformance.rs b/store-postgres-plugin/tests/conformance.rs index 0b1d0a4..574f304 100644 --- a/store-postgres-plugin/tests/conformance.rs +++ b/store-postgres-plugin/tests/conformance.rs @@ -26,10 +26,41 @@ mod common; // THE PUBLISHED SUITE (busbar-plugin-loader's `conformance` feature, at the pin): the linked door and // the built cdylib, each through the one loader, driven by the store kind's script over the live // Postgres `conformance.json` names; exact crossing counts, the two folds equal, its RED arms. +// +// Each fold opens over its own schema: conformance.json's url names `search_path={fold}`, and the +// hooks below create that schema before the fold and drop it after, on an independent connection +// of the `postgres` driver (the store never creates a schema; nothing in its behaviour changes). busbar_plugin_loader::conformance_suite! { door: busbar_store_postgres::door, cdylib: "busbar_store_postgres_plugin", inputs: include_str!("conformance.json"), + namespace: (create_fold_schema, drop_fold_schema), +} + +/// The live server a fold's filled settings name, on a connection of the `postgres` driver's own. +fn fold_client(settings: &[u8]) -> postgres::Client { + let v: serde_json::Value = + serde_json::from_slice(settings).expect("conformance.json's settings are JSON"); + let url = v["url"] + .as_str() + .expect("conformance.json's settings name a url"); + postgres::Client::connect(url, postgres::NoTls) + .expect("the live Postgres accepts the test client") +} + +/// The suite's namespace hook: the fold's schema, made before its open. +fn create_fold_schema(namespace: &str, settings: &[u8]) { + fold_client(settings) + .batch_execute(&format!("CREATE SCHEMA IF NOT EXISTS \"{namespace}\"")) + .expect("the fold's schema is created"); +} + +/// The suite's namespace hook: the fold's schema and everything the store made in it, dropped after +/// the fold. +fn drop_fold_schema(namespace: &str, settings: &[u8]) { + fold_client(settings) + .batch_execute(&format!("DROP SCHEMA IF EXISTS \"{namespace}\" CASCADE")) + .expect("the fold's schema is dropped"); } use busbar_contract::records::{PlaneDisposition, PlaneRecord, PlaneSelector, RecordStore}; From bc5a9ddce9339285cf05c82d434de48e67cc52fb Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:16:08 -0700 Subject: [PATCH 16/16] conformance: sslmode through the host's TLS (ARCHITECT Q-P4-9): the suite's host connector, a TLS front over the live Postgres, pinned to busbar lane-p4-fleet-doors daf4c0275e - declares.json states the store's network need (`needs: ["tcp"]`), the data the fleet render reads. - Rendered by busbar-release #162 (with #161's parity-lines, merged locally) at `plugin render busbar-store-postgres --tree . --out . --pin daf4c0275e... 1.6.0`: the pin, the host adapter (store-postgres-plugin/tests/support/conformance_host.rs), its dev-dependency rows (busbar-core-connector with test-support, busbar-transport-tcp at busbar's rev), the one-contract [patch], deny's sources; Cargo.lock re-resolved from busbar's lock at the pin. - conformance.json's url names the suite's TLS front (localhost:52670) with sslmode=require; the store's SSLRequest is answered by the front, the TLS is the HOST's (upgrade_secure through busbar's connector), verified against the suite's test CA (`tls: conformance_host::anchors()`, the CA that signed the front's certificate), and the front carries the secured connection to the live Postgres (BUSBAR_TEST_POSTGRES_URL's authority). The namespace hooks reach the live server directly, in the clear, on the test's own client. Local (postgres 17 on loopback): fmt, clippy -D warnings, cargo deny, pin-check, the conformance target debug and --release 11/11; RED: with `tls:` withheld the store does not open (both_ways panics at the open); BUSBAR_CONFORMANCE_RED=count fails with "crossing(s), pinned". --- .busbar-ref | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/consumer-verify.yml | 2 +- .github/workflows/release.yml | 4 +- .github/workflows/repin.yml | 2 +- Cargo.lock | 916 +++++++++++++++++- Cargo.toml | 23 +- deny.toml | 4 +- store-postgres-plugin/Cargo.toml | 6 + store-postgres-plugin/tests/conformance.json | 2 +- store-postgres-plugin/tests/conformance.rs | 53 +- .../tests/support/conformance_host.rs | 521 ++++++++++ store-postgres/declares.json | 5 +- 13 files changed, 1505 insertions(+), 37 deletions(-) create mode 100644 store-postgres-plugin/tests/support/conformance_host.rs diff --git a/.busbar-ref b/.busbar-ref index 407b821..a483331 100644 --- a/.busbar-ref +++ b/.busbar-ref @@ -1 +1 @@ -c4ed9531489031942edc1ca5e43377904bc73f80 1.6.0 +daf4c0275e734fe1988053e241486042351b8fb3 1.6.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8714d5a..064a1e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ permissions: jobs: ci: - uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@c4ed9531489031942edc1ca5e43377904bc73f80 + uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@daf4c0275e734fe1988053e241486042351b8fb3 with: service: postgres busbar_checkout: true diff --git a/.github/workflows/consumer-verify.yml b/.github/workflows/consumer-verify.yml index d34c6c4..fca9fd2 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@c4ed9531489031942edc1ca5e43377904bc73f80 + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@daf4c0275e734fe1988053e241486042351b8fb3 with: version: ${{ inputs.version || '' }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 61e86d6..23c1567 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: secrets: inherit release: needs: ci - uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@c4ed9531489031942edc1ca5e43377904bc73f80 + uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@daf4c0275e734fe1988053e241486042351b8fb3 with: plugin_crate: busbar-store-postgres-plugin manifest_name: busbar-store-postgres @@ -41,7 +41,7 @@ jobs: consumer-verify: needs: release if: ${{ !cancelled() }} - uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@c4ed9531489031942edc1ca5e43377904bc73f80 + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@daf4c0275e734fe1988053e241486042351b8fb3 with: version: ${{ github.ref_name }} asset_prefix: busbar-store-postgres diff --git a/.github/workflows/repin.yml b/.github/workflows/repin.yml index 0517e51..df06a5e 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@c4ed9531489031942edc1ca5e43377904bc73f80 + uses: GetBusbar/busbar/.github/workflows/plugin-repin.yml@daf4c0275e734fe1988053e241486042351b8fb3 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 a62cc82..c771c21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,43 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + [[package]] name = "asn1-rs" version = "0.7.2" @@ -17,7 +54,7 @@ dependencies = [ "asn1-rs-derive", "asn1-rs-impl", "displaydoc", - "nom", + "nom 7.1.3", "num-traits", "rusticata-macros", "thiserror", @@ -70,12 +107,70 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -124,10 +219,10 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "busbar-contract" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=c4ed9531489031942edc1ca5e43377904bc73f80#c4ed9531489031942edc1ca5e43377904bc73f80" +source = "git+https://www.github.com/GetBusbar/busbar?rev=daf4c0275e734fe1988053e241486042351b8fb3#daf4c0275e734fe1988053e241486042351b8fb3" dependencies = [ "async-trait", - "base64", + "base64 0.22.1", "futures", "hex", "http", @@ -144,20 +239,144 @@ dependencies = [ "zeroize", ] +[[package]] +name = "busbar-core-connector" +version = "1.6.0" +source = "git+https://github.com/GetBusbar/busbar?rev=daf4c0275e734fe1988053e241486042351b8fb3#daf4c0275e734fe1988053e241486042351b8fb3" +dependencies = [ + "busbar-contract", + "busbar-kernel", + "crc32fast", + "dimpl", + "futures", + "libc", + "rcgen", + "ring", + "rustls", + "rustls-pki-types", + "rustls-webpki", + "socket2", + "tokio", + "tokio-rustls", + "tokio-util", + "webpki-roots", + "zeroize", +] + +[[package]] +name = "busbar-kernel" +version = "1.6.0" +source = "git+https://github.com/GetBusbar/busbar?rev=daf4c0275e734fe1988053e241486042351b8fb3#daf4c0275e734fe1988053e241486042351b8fb3" +dependencies = [ + "arc-swap", + "async-trait", + "axum", + "base64 0.22.1", + "bumpalo", + "busbar-contract", + "busbar-kernel-audit", + "busbar-kernel-breaker", + "busbar-kernel-egress", + "busbar-kernel-identity", + "busbar-kernel-ledger", + "busbar-kernel-scope", + "busbar-kernel-wal", + "busbar-plugin-loader", + "bytes", + "crc32fast", + "ed25519-dalek", + "futures", + "getrandom 0.3.4", + "hex", + "http", + "http-body", + "http-body-util", + "httpdate", + "hyper", + "hyper-util", + "indexmap", + "metrics", + "metrics-exporter-prometheus", + "metrics-util", + "ring", + "serde", + "serde_json", + "serde_urlencoded", + "serde_yaml_ng", + "sonic-rs", + "tokio", + "tokio-util", + "tower", + "tracing", + "tracing-subscriber", + "url", + "zeroize", +] + +[[package]] +name = "busbar-kernel-audit" +version = "1.6.0" +source = "git+https://github.com/GetBusbar/busbar?rev=daf4c0275e734fe1988053e241486042351b8fb3#daf4c0275e734fe1988053e241486042351b8fb3" +dependencies = [ + "busbar-contract", + "ed25519-dalek", + "ring", + "zeroize", +] + +[[package]] +name = "busbar-kernel-breaker" +version = "1.6.0" +source = "git+https://github.com/GetBusbar/busbar?rev=daf4c0275e734fe1988053e241486042351b8fb3#daf4c0275e734fe1988053e241486042351b8fb3" +dependencies = [ + "busbar-contract", + "httpdate", +] + +[[package]] +name = "busbar-kernel-egress" +version = "1.6.0" +source = "git+https://github.com/GetBusbar/busbar?rev=daf4c0275e734fe1988053e241486042351b8fb3#daf4c0275e734fe1988053e241486042351b8fb3" +dependencies = [ + "busbar-contract", + "bytes", + "futures", +] + +[[package]] +name = "busbar-kernel-identity" +version = "1.6.0" +source = "git+https://github.com/GetBusbar/busbar?rev=daf4c0275e734fe1988053e241486042351b8fb3#daf4c0275e734fe1988053e241486042351b8fb3" +dependencies = [ + "busbar-contract", + "hex", + "ring", + "serde", + "tracing", +] + [[package]] name = "busbar-kernel-ledger" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=c4ed9531489031942edc1ca5e43377904bc73f80#c4ed9531489031942edc1ca5e43377904bc73f80" +source = "git+https://github.com/GetBusbar/busbar?rev=daf4c0275e734fe1988053e241486042351b8fb3#daf4c0275e734fe1988053e241486042351b8fb3" dependencies = [ "busbar-contract", "ring", "serde", ] +[[package]] +name = "busbar-kernel-scope" +version = "1.6.0" +source = "git+https://github.com/GetBusbar/busbar?rev=daf4c0275e734fe1988053e241486042351b8fb3#daf4c0275e734fe1988053e241486042351b8fb3" +dependencies = [ + "busbar-contract", +] + [[package]] name = "busbar-kernel-wal" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=c4ed9531489031942edc1ca5e43377904bc73f80#c4ed9531489031942edc1ca5e43377904bc73f80" +source = "git+https://github.com/GetBusbar/busbar?rev=daf4c0275e734fe1988053e241486042351b8fb3#daf4c0275e734fe1988053e241486042351b8fb3" dependencies = [ "busbar-contract", "ring", @@ -166,7 +385,7 @@ dependencies = [ [[package]] name = "busbar-plugin-loader" version = "1.5.0" -source = "git+https://github.com/GetBusbar/busbar?rev=c4ed9531489031942edc1ca5e43377904bc73f80#c4ed9531489031942edc1ca5e43377904bc73f80" +source = "git+https://github.com/GetBusbar/busbar?rev=daf4c0275e734fe1988053e241486042351b8fb3#daf4c0275e734fe1988053e241486042351b8fb3" dependencies = [ "busbar-contract", "busbar-kernel-ledger", @@ -207,13 +426,24 @@ dependencies = [ name = "busbar-store-postgres-plugin" version = "1.0.0" dependencies = [ - "base64", + "base64 0.22.1", "busbar-contract", + "busbar-core-connector", "busbar-plugin-loader", "busbar-store-postgres", + "busbar-transport-tcp", "postgres", "reqwest", "serde_json", + "tokio", +] + +[[package]] +name = "busbar-transport-tcp" +version = "1.6.0" +source = "git+https://github.com/GetBusbar/busbar-transport-tcp?rev=89d08ebcb91e5b2d4001b667fba151d9bc09d22d#89d08ebcb91e5b2d4001b667fba151d9bc09d22d" +dependencies = [ + "busbar-contract", ] [[package]] @@ -306,6 +536,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.7" @@ -385,7 +630,7 @@ checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ "asn1-rs", "displaydoc", - "nom", + "nom 7.1.3", "num-bigint", "num-traits", "rusticata-macros", @@ -419,6 +664,21 @@ dependencies = [ "ctutils", ] +[[package]] +name = "dimpl" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07d467d37bfdcc290bb8dcbedc8ca356eb730e46459b1687f98c3bae28aa6814" +dependencies = [ + "arrayvec", + "log", + "nom 8.0.0", + "once_cell", + "rand 0.9.4", + "subtle", + "time", +] + [[package]] name = "displaydoc" version = "0.2.6" @@ -460,12 +720,45 @@ 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.61.2", +] + +[[package]] +name = "evmap" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8874945f036109c72242964c1174cf99434e30cfa45bf45fedc983f50046f8" +dependencies = [ + "hashbag", + "left-right", + "smallvec", +] + [[package]] name = "fallible-iterator" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "faststr" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca7d44d22004409a61c393afb3369c8f7bb74abcae49fe249ee01dcc3002113" +dependencies = [ + "bytes", + "rkyv", + "serde", + "simdutf8", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -498,6 +791,18 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -595,6 +900,21 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -644,6 +964,40 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "h2" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c0e8a181239723652be9062bb56ca5bf5f64011f73b623f6f4fc59086a228" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbag" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7040a10f52cba493ddb09926e15d10a9d8a28043708a405931fe4c6f19fac064" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -704,6 +1058,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hybrid-array" version = "0.4.15" @@ -723,9 +1083,11 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -755,7 +1117,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -810,7 +1172,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -845,6 +1207,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "left-right" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0c21e4c8ff95f487fb34e6f9182875f42c84cef966d29216bf115d9bba835a" +dependencies = [ + "crossbeam-utils", + "loom", + "slab", +] + [[package]] name = "libc" version = "0.2.189" @@ -885,12 +1258,40 @@ version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.11.0" @@ -907,6 +1308,54 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +[[package]] +name = "metrics" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89550ee9f79e88fef3119de263694973a8adb26c21d75322164fb8c493039fe2" +dependencies = [ + "portable-atomic", + "rapidhash", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db0d8f1fc9e62caebd0319e11eaec5822b0186c171568f0480b46a0137f9108" +dependencies = [ + "base64 0.22.1", + "evmap", + "indexmap", + "metrics", + "metrics-util", + "quanta", + "thiserror", +] + +[[package]] +name = "metrics-util" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96f8722f8562635f92f8ed992f26df0532266eb03d5202607c20c0d7e9745e13" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.16.1", + "metrics", + "quanta", + "rand 0.9.4", + "rand_xoshiro", + "rapidhash", + "sketches-ddsketch", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -934,14 +1383,52 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nom" -version = "7.1.3" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" dependencies = [ "memchr", - "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", ] [[package]] @@ -1034,6 +1521,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pem" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" +dependencies = [ + "base64 0.23.1", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1075,6 +1572,12 @@ dependencies = [ "spki", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "postgres" version = "0.19.14" @@ -1095,14 +1598,14 @@ version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514" dependencies = [ - "base64", + "base64 0.22.1", "byteorder", "bytes", "fallible-iterator", "hmac", "md-5", "memchr", - "rand", + "rand 0.10.2", "sha2 0.11.0", "stringprep", ] @@ -1124,6 +1627,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1133,6 +1645,41 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi 0.11.1+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quinn" version = "0.11.9" @@ -1162,7 +1709,7 @@ dependencies = [ "bytes", "getrandom 0.4.3", "lru-slab", - "rand", + "rand 0.10.2", "rand_pcg", "ring", "rustc-hash", @@ -1210,6 +1757,25 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rancor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +dependencies = [ + "ptr_meta", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.2" @@ -1221,6 +1787,16 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -1230,6 +1806,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_core" version = "0.10.1" @@ -1245,12 +1830,40 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rapidhash" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +dependencies = [ + "rustversion", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + [[package]] name = "rcgen" version = "0.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8" dependencies = [ + "pem", "ring", "rustls-pki-types", "time", @@ -1267,13 +1880,56 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rend" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" + [[package]] name = "reqwest" version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1323,6 +1979,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9776093b7ca170454ab1406954f7b7d97a57c51dc6c0642957fb2ef25c2d399" +dependencies = [ + "bytes", + "hashbrown 0.17.1", + "indexmap", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c25ef604ac7dd839d44d64648952ea23c97866f124ff671b0ed2cf3ad9bb06e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -1344,7 +2029,7 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -1394,6 +2079,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -1449,6 +2140,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1496,12 +2198,31 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -1517,12 +2238,24 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" +[[package]] +name = "sketches-ddsketch" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b" + [[package]] name = "slab" version = "0.4.12" @@ -1545,6 +2278,45 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "sonic-number" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c350a05f295f922fc153a3af8b443c10db70904a9291a624ddb6058c6110d0" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "sonic-rs" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756586caeeb21126a57c17d1ef2b9386e2613ba784aae0f27104f5e9982335ec" +dependencies = [ + "ahash", + "bumpalo", + "bytes", + "cfg-if", + "faststr", + "itoa", + "ref-cast", + "serde", + "simdutf8", + "sonic-number", + "sonic-simd", + "thiserror", + "zmij", +] + +[[package]] +name = "sonic-simd" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f99e664ecd2d85a68c87e3c7a3cfe691f647ea9e835de984aba4d54a41f817d4" +dependencies = [ + "cfg-if", +] + [[package]] name = "spki" version = "0.7.3" @@ -1644,6 +2416,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.49" @@ -1699,10 +2480,23 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "tokio-postgres" version = "0.7.18" @@ -1722,7 +2516,7 @@ dependencies = [ "pin-project-lite", "postgres-protocol", "postgres-types", - "rand", + "rand 0.10.2", "socket2", "tokio", "tokio-util", @@ -1747,6 +2541,7 @@ checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -1765,6 +2560,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -1803,6 +2599,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1840,6 +2637,24 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -1915,6 +2730,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -2079,12 +2904,43 @@ dependencies = [ "web-sys", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2183,7 +3039,7 @@ dependencies = [ "data-encoding", "der-parser", "lazy_static", - "nom", + "nom 7.1.3", "oid-registry", "ring", "rusticata-macros", @@ -2201,6 +3057,26 @@ dependencies = [ "time", ] +[[package]] +name = "zerocopy" +version = "0.8.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "zeroize" version = "1.9.0" diff --git a/Cargo.toml b/Cargo.toml index 5542683..156b8fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,8 @@ # `.busbar-ref`; no sibling-checkout path dependency ships in any manifest. # # THIRD-PARTY VERSIONS ARE THE FLEET'S, NOT THIS REPO'S. The table below is busbar's own root -# `[workspace.dependencies]` (third-party rows) at busbar c4ed9531489031942edc1ca5e43377904bc73f80, followed by the plugin-only rows -# of busbar's `.github/fleet/deps.toml` at c4ed9531489031942edc1ca5e43377904bc73f80. A member takes a crate with +# `[workspace.dependencies]` (third-party rows) at busbar daf4c0275e734fe1988053e241486042351b8fb3, followed by the plugin-only rows +# of busbar's `.github/fleet/deps.toml` at daf4c0275e734fe1988053e241486042351b8fb3. A member takes a crate with # `{ workspace = true }` (adding features if it needs them); a crate in neither table is a new # `deps.toml` row in busbar first. CI holds this repo's Cargo.lock to busbar's lock at the pin. [workspace] @@ -29,9 +29,14 @@ publish = false repository = "https://github.com/GetBusbar/busbar-store-postgres" [workspace.dependencies] -busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "c4ed9531489031942edc1ca5e43377904bc73f80" } -busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "c4ed9531489031942edc1ca5e43377904bc73f80" } -# busbar's root [workspace.dependencies], third-party rows, at c4ed9531489031942edc1ca5e43377904bc73f80 +busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "daf4c0275e734fe1988053e241486042351b8fb3" } +busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "daf4c0275e734fe1988053e241486042351b8fb3" } +# THE SUITE'S HOST CONNECTOR (dev-only; store-postgres-plugin/tests/support/conformance_host.rs, +# rendered because the declares file states network needs): busbar's connector and its TLS +# test kit at the pin, and the transport framers those needs name, at the revs busbar links. +busbar-core-connector = { git = "https://github.com/GetBusbar/busbar", rev = "daf4c0275e734fe1988053e241486042351b8fb3", features = ["test-support"] } +busbar-transport-tcp = { git = "https://github.com/GetBusbar/busbar-transport-tcp", rev = "89d08ebcb91e5b2d4001b667fba151d9bc09d22d" } +# busbar's root [workspace.dependencies], third-party rows, at daf4c0275e734fe1988053e241486042351b8fb3 a2a-lf = "0.3.0" a2a-pb = "0.2.0" arc-swap = "1" @@ -107,7 +112,7 @@ url = "2" webpki = { package = "rustls-webpki", version = "0.103", default-features = false, features = ["std", "ring"] } webpki-roots = "1" zeroize = "1" -# busbar .github/fleet/deps.toml [plugin-deps], at c4ed9531489031942edc1ca5e43377904bc73f80 +# busbar .github/fleet/deps.toml [plugin-deps], at daf4c0275e734fe1988053e241486042351b8fb3 ldap3 = { version = "0.12", default-features = false, features = ["sync"] } mysql = { version = "28", default-features = false, features = ["minimal-rust"] } mysql_common = { version = "0.35", default-features = false } @@ -117,3 +122,9 @@ redis = { version = "1", default-features = false, features = ["script"] } rusqlite = { version = "0.40", features = ["bundled"] } ulid = "1" +# ONE busbar-contract (the suite's host connector): the transport framers above name +# busbar-contract at their own revs of busbar; this repo's pin serves them all, as busbar +# patches them to its own tree. The source is busbar spelled through www.github.com: Cargo +# refuses a patch that names the source it patches. +[patch."https://github.com/GetBusbar/busbar"] +busbar-contract = { git = "https://www.github.com/GetBusbar/busbar", rev = "daf4c0275e734fe1988053e241486042351b8fb3" } diff --git a/deny.toml b/deny.toml index b05cf61..b935439 100644 --- a/deny.toml +++ b/deny.toml @@ -1,5 +1,5 @@ # RENDERED by `busbar-release plugin sync` from GetBusbar/busbar-release template/ and busbar's -# `.github/fleet/deps.toml` at c4ed9531489031942edc1ca5e43377904bc73f80; a hand edit is overwritten by the next sync. CI runs +# `.github/fleet/deps.toml` at daf4c0275e734fe1988053e241486042351b8fb3; a hand edit is overwritten by the next sync. CI runs # `cargo deny check` on it (busbar's plugin-ci.yml at the pin). The license allowlist is busbar's own; # the one reviewed git source is busbar itself, at the pin. [graph] @@ -247,4 +247,4 @@ skip = [ unknown-registry = "deny" unknown-git = "deny" allow-registry = ["https://github.com/rust-lang/crates.io-index"] -allow-git = ["https://github.com/GetBusbar/busbar"] +allow-git = ["https://github.com/GetBusbar/busbar", "https://www.github.com/GetBusbar/busbar", "https://github.com/GetBusbar/busbar-transport-tcp"] diff --git a/store-postgres-plugin/Cargo.toml b/store-postgres-plugin/Cargo.toml index 45aeb01..2cfa041 100644 --- a/store-postgres-plugin/Cargo.toml +++ b/store-postgres-plugin/Cargo.toml @@ -41,3 +41,9 @@ postgres = { workspace = true } # in-process axum server. reqwest = { workspace = true, features = ["blocking", "json"] } base64 = "0.22" +# THE SUITE'S HOST CONNECTOR (tests/support/conformance_host.rs, rendered by the fleet template for a +# repo whose declares state network needs): busbar's connector and its TLS test kit, and the tcp +# framer the store's need names. Dev-only. +busbar-core-connector = { workspace = true } +busbar-transport-tcp = { workspace = true } +tokio = { workspace = true, features = ["rt"] } diff --git a/store-postgres-plugin/tests/conformance.json b/store-postgres-plugin/tests/conformance.json index d23e15c..89f8c4d 100644 --- a/store-postgres-plugin/tests/conformance.json +++ b/store-postgres-plugin/tests/conformance.json @@ -1,5 +1,5 @@ { - "settings": { "url": "postgres://busbar:busbar@localhost:5432/busbar_test?options=-c%20search_path%3D{fold}" }, + "settings": { "url": "postgres://busbar:busbar@localhost:52670/busbar_test?sslmode=require&options=-c%20search_path%3D{fold}" }, "store": { "credential_kind": "sigv4", "node": 7, diff --git a/store-postgres-plugin/tests/conformance.rs b/store-postgres-plugin/tests/conformance.rs index 574f304..40725da 100644 --- a/store-postgres-plugin/tests/conformance.rs +++ b/store-postgres-plugin/tests/conformance.rs @@ -30,13 +30,60 @@ mod common; // Each fold opens over its own schema: conformance.json's url names `search_path={fold}`, and the // hooks below create that schema before the fold and drop it after, on an independent connection // of the `postgres` driver (the store never creates a schema; nothing in its behaviour changes). +// +// THE HOST (ARCHITECT Q-P4-9): the store's `tcp` need is served by busbar's own connector, composed +// as the root composes it (`conformance_host`, rendered by the fleet template), and the store asks +// for TLS (`sslmode=require`): its `SSLRequest` is answered by the suite's TLS front, whose +// certificate chains to the suite's test CA, the anchors only the HOST's TLS is handed (`tls:`); +// the front carries the secured connection to the live Postgres. So every fold proves the store's +// connection is secured by the host, verified against the anchors. +#[path = "support/conformance_host.rs"] +mod conformance_host; + busbar_plugin_loader::conformance_suite! { door: busbar_store_postgres::door, cdylib: "busbar_store_postgres_plugin", inputs: include_str!("conformance.json"), + host: host, + tls: conformance_host::anchors(), namespace: (create_fold_schema, drop_fold_schema), } +/// The live Postgres's address (`BUSBAR_TEST_POSTGRES_URL`'s authority; the service container's +/// default when unset). +fn upstream() -> &'static str { + static AT: std::sync::OnceLock = std::sync::OnceLock::new(); + AT.get_or_init(|| { + let url = std::env::var("BUSBAR_TEST_POSTGRES_URL").unwrap_or_default(); + url.split_once("://") + .map(|(_, rest)| rest) + .and_then(|rest| rest.rsplit_once('@').map_or(Some(rest), |(_, at)| Some(at))) + .and_then(|at| at.split('/').next()) + .filter(|at| !at.is_empty()) + .unwrap_or("localhost:5432") + .to_owned() + }) +} + +/// Postgres's cleartext negotiation: the client's `SSLRequest` (length 8, code 80877103) answered +/// `S`; anything else is not a TLS client. +fn ssl_request(tcp: &mut std::net::TcpStream) -> bool { + use std::io::{Read, Write}; + let mut req = [0_u8; 8]; + tcp.read_exact(&mut req).is_ok() + && req == [0, 0, 0, 8, 0x04, 0xd2, 0x16, 0x2f] + && tcp.write_all(b"S").is_ok() +} + +/// The host the suite binds the store over, with the TLS front its settings name already listening. +fn host( + wake: std::sync::Arc, + anchors: Option<&str>, +) -> std::sync::Arc { + conformance_host::tls_front(ssl_request, upstream()); + conformance_host::host(wake, anchors) +} + /// The live server a fold's filled settings name, on a connection of the `postgres` driver's own. fn fold_client(settings: &[u8]) -> postgres::Client { let v: serde_json::Value = @@ -44,7 +91,11 @@ fn fold_client(settings: &[u8]) -> postgres::Client { let url = v["url"] .as_str() .expect("conformance.json's settings name a url"); - postgres::Client::connect(url, postgres::NoTls) + // Straight to the live server, in the clear: the TLS front is the store's, not this client's. + let url = url + .replace(conformance_host::FAR_END, upstream()) + .replace("sslmode=require", "sslmode=disable"); + postgres::Client::connect(&url, postgres::NoTls) .expect("the live Postgres accepts the test client") } diff --git a/store-postgres-plugin/tests/support/conformance_host.rs b/store-postgres-plugin/tests/support/conformance_host.rs new file mode 100644 index 0000000..069e5d3 --- /dev/null +++ b/store-postgres-plugin/tests/support/conformance_host.rs @@ -0,0 +1,521 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors +// +// RENDERED by `busbar-release plugin sync busbar-store-postgres` from GetBusbar/busbar-release +// template/conformance-host/, because this repo's declares file states network needs (`needs`: +// `tcp`); a hand edit is overwritten by the next sync. + +//! **THE SUITE'S HOST CONNECTOR, IN THIS PLUGIN'S OWN TEST** (ARCHITECT Q-P4-9 (d)). busbar keeps +//! only the neutral seam (`busbar_plugin_loader::conformance::Host`, the suite's `host:` and `tls:` +//! arguments); this adapter implements it the way busbar's composition root composes the process's +//! one connector — carrier -> [TLS] -> framer (`BUSBAR-1.6.0.md` THE DESIGN §5): +//! +//! * busbar's own `Connector` (`busbar-core-connector`, a dev-dependency at the pin), its framer +//! entries the transport doors this plugin's needs name (`tcp`), each loaded +//! through the loader's one load and opened as the root opens a transport door; +//! * the connector's own TLS (connsec), trusting the platform roots plus the suite's TEST trust +//! anchors (`tls:`), never handed to the plugin: no plugin holds a key, a certificate or a TLS +//! type (TRANSPORT-STACK (1)); +//! * a dial judge that admits loopback only: the suite dials the REAL local endpoints its settings +//! name, never the network; +//! * a parked read wakes the plugin's ticket through the leg dispatcher's conn waker. +//! +//! [`far_end`] is a TLS far end at [`FAR_END`] whose certificate chains to [`anchors`] (busbar's +//! test kit, `busbar_core_connector::test_support`): the plugin's `conformance.json` settings name +//! it, and its `answer` speaks the plugin's protocol. Its own tests, below, run with the suite: +//! a TLS upgrade verifies against the anchors, and is REFUSED with the anchors withheld (RED). +//! +//! The plugin's `tests/conformance.rs` mounts this file and names it: +//! +//! ```ignore +//! #[path = "support/conformance_host.rs"] +//! mod conformance_host; +//! +//! busbar_plugin_loader::conformance_suite! { +//! door: …, cdylib: …, inputs: include_str!("conformance.json"), +//! host: conformance_host::host, +//! tls: conformance_host::anchors(), +//! } +//! ``` +//! +//! with the dev-dependencies the rendered root `Cargo.toml` states (`{ workspace = true }`): +//! `busbar-core-connector`, `busbar-transport-tcp`, `tokio`. + +#![allow(dead_code)] + +use std::io::{Read, Write}; +use std::net::{SocketAddr, TcpListener, TcpStream, ToSocketAddrs}; +use std::sync::{Arc, OnceLock}; +use std::time::{Duration, Instant}; + +use busbar_contract::abi::host::conn::connector::{ + DIRECTION_OUTBOUND, EGRESS_OPERATOR_INFRASTRUCTURE, +}; +use busbar_contract::abi::mechanism::call::{Blob, Outcome, BLOB_ABSENT}; +use busbar_contract::abi::mechanism::door::DoorFn; +use busbar_contract::abi::mechanism::lifecycle::{slot as life, OpenIn, OpenOut}; +use busbar_contract::abi::mechanism::rendering::{ReadBlob, ReadNeed}; +use busbar_contract::abi::sdk::door::{blank_in, blank_out}; +use busbar_contract::abi::transport::slot; +use busbar_contract::conn::{ + ConnError, ConnId, DeclaredConns, InstanceId, NeedId, OpenDesc, PieceKind, NO_TICKET, +}; +use busbar_contract::transport::EgressTrust; +use busbar_core_connector::framer::{Call, Crossed, DoorFacts, FramerDoor}; +use busbar_core_connector::registry::{Entry, Transports}; +use busbar_core_connector::test_support::{private_ca, ServerTls, StdServerSession, TestCa}; +use busbar_core_connector::{Connector, Judged, Verdict}; +use busbar_plugin_loader::dispatch::kinds::transport::{Transport, TransportFacts}; +use busbar_plugin_loader::dispatch::{ + load_linked, Bind, ConnTable, DispatchConfig, Dispatcher, Frame, InFrame, LinkedRow, NoSink, + OutFrame, Plugin, +}; + +/// Where [`far_end`] listens: loopback, a port of this repo's own. +pub const FAR_END: &str = "localhost:52670"; + +/// The framer doors the host serves, as the root links them: (claim, door). One row a line, +/// whatever the claims' lengths (a `vec!` would be reflowed by rustfmt). +#[allow(clippy::vec_init_then_push)] +fn doors() -> Vec<(&'static str, DoorFn)> { + use busbar_transport_tcp::linked::door as tcp_door; + // Each row is compiled against this repo's own pin of busbar-contract (the rendered `[patch]`), + // as busbar compiles the rows it links. + let mut doors: Vec<(&'static str, DoorFn)> = Vec::new(); + doors.push(("tcp", tcp_door)); + doors +} + +/// The suite's test CA: [`anchors`] is its certificate, [`far_end`] serves a leaf it signed. +fn ca() -> &'static TestCa { + static CA: OnceLock = OnceLock::new(); + CA.get_or_init(private_ca) +} + +/// THE SUITE'S TEST TRUST ANCHORS (PEM), for `conformance_suite!`'s `tls:`. +#[must_use] +pub fn anchors() -> &'static str { + &ca().ca_pem +} + +/// THE HOST CONNECTOR (`busbar_plugin_loader::conformance::Host`): one per leg. +/// +/// # Panics +/// The anchors do not parse, a transport door does not load or open, or the view does not compose. +#[must_use] +pub fn host(wake: Arc, anchors: Option<&str>) -> Arc { + // The reactor a socket a plugin opens from a dispatcher worker registers on, as the root + // installs it (`root::connector::io_reactor`): the connector's own I/O thread. + busbar_core_connector::io::install_process_reactor(io_reactor()); + let trust = EgressTrust { + extra_anchors: anchors.map(pem_certs).unwrap_or_default(), + ..EgressTrust::default() + }; + let tls = busbar_core_connector::tls::client::build_client_config(&trust) + .unwrap_or_else(|e| panic!("the host's connection security does not build: {e}")); + let view = Transports::new(entries()) + .unwrap_or_else(|e| panic!("the host's transport doors do not compose: {e}")); + Arc::new(Connector::serving( + view, + Arc::new(loopback), + Some(Arc::new(tls)), + wake, + )) +} + +/// The connector's I/O thread: a single-threaded runtime of its own whose reactor drives every +/// socket the plugin opens from a dispatcher worker. Built once. +fn io_reactor() -> tokio::runtime::Handle { + static IO: OnceLock = OnceLock::new(); + IO.get_or_init(|| { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("the host's I/O runtime builds"); + let handle = rt.handle().clone(); + std::thread::Builder::new() + .name("conformance-host-io".into()) + .spawn(move || rt.block_on(std::future::pending::<()>())) + .expect("the host's I/O thread starts"); + handle + }) + .clone() +} + +/// THE DIAL JUDGE: a loopback address (a literal, or a name that resolves to one) and nothing else. +fn loopback(dest: &str, _class: u32, _done: Judged) -> Option> { + let refused = busbar_contract::abi::host::service::DEST_UNRESOLVABLE; + let at = dest + .to_socket_addrs() + .ok() + .and_then(|mut a| a.find(|a| a.ip().is_loopback())); + Some(at.ok_or(refused)) +} + +/// Every certificate in `pem`, DER. +fn pem_certs(pem: &str) -> Vec> { + let certs = busbar_core_connector::test_support::certs_from_pem(pem); + assert!( + !certs.is_empty(), + "the suite's trust anchors hold no certificate" + ); + certs +} + +/// The dispatcher the host's transport doors are adopted by. +fn transport_dispatcher() -> &'static Dispatcher { + static ONE: OnceLock = OnceLock::new(); + ONE.get_or_init(|| Dispatcher::new(DispatchConfig::default())) +} + +/// The framer entries: each door loaded through the one loader and opened with no settings, as +/// the root opens a transport door that declares none it reads. +fn entries() -> Vec { + doors() + .into_iter() + .map(|(claim, door)| { + let bind = Bind { + instance: Arc::from(claim), + max_inflight_cap: 1024, + sink: Arc::new(NoSink), + dispatcher: transport_dispatcher().adopter(), + // A transport door is a framer the connector drives: it declares no need. + conns: ConnTable::NoNeeds, + }; + let plugin = LinkedRow::of(door) + .and_then(|row| load_linked::(&row, bind)) + .unwrap_or_else(|e| panic!("the host's transport `{claim}` does not load: {e}")); + Entry { + door: Arc::new(Opened::open(plugin, claim)), + alpn: Vec::new(), + } + }) + .collect() +} + +/// One transport door, opened, as the connector reaches it (the root's `doors::Dispatched`). +struct Opened { + plugin: Plugin, + facts: DoorFacts, +} + +impl Opened { + fn open(plugin: Plugin, claim: &str) -> Self { + let stated = plugin + .context::() + .cloned() + .unwrap_or_else(|| panic!("the host's transport `{claim}` states no transport tail")); + let mut i: OpenIn = blank_in(); + i.settings = Blob { + ptr: std::ptr::null(), + len: 0, + fmt: BLOB_ABSENT, + flags: 0, + }; + let mut f = Frame::new(i, blank_out::()); + let opened = plugin.call(life::OPEN, &mut f); + assert_eq!( + opened.outcome, + Outcome::Ready, + "the host's transport `{claim}` does not open" + ); + plugin + .ready(transport_dispatcher(), Duration::from_secs(10)) + .unwrap_or_else(|e| panic!("the host's transport `{claim}` is not ready: {e}")); + let facts = DoorFacts { + name: plugin.name().to_owned(), + claims: stated.claims, + composes_over: stated.composes_over, + }; + Self { plugin, facts } + } +} + +/// One crossing through the dispatcher: the host's `in`/`out` copied in, the answer copied back. +fn go(p: &Plugin, s: u32, i: &mut I, o: &mut O) -> Crossed { + let mut f = Frame::new(*i, *o); + let c = p.call(s, &mut f); + *i = f.input; + *o = f.out; + Crossed { + outcome: c.outcome, + error: c.error, + } +} + +impl FramerDoor for Opened { + fn facts(&self) -> &DoorFacts { + &self.facts + } + + fn cross(&self, call: Call<'_>) -> Crossed { + let p = &self.plugin; + match call { + Call::Locate(i, o) => go(p, slot::LOCATE, i, o), + Call::Begin(i, o) => go(p, slot::BEGIN, i, o), + Call::Ingest(i, o) => go(p, slot::INGEST, i, o), + Call::Emit(i, o) => go(p, slot::EMIT, i, o), + Call::Encode(i, o) => go(p, slot::ENCODE, i, o), + Call::Refuse(i, o) => go(p, slot::REFUSE, i, o), + Call::Finish(i, o) => go(p, slot::FINISH, i, o), + Call::Detach(i, o) => go(p, slot::DETACH, i, o), + Call::Adopt(i, o) => go(p, slot::ADOPT, i, o), + Call::Timer(i, o) => go(p, slot::TIMER, i, o), + } + } +} + +// ---- the far end ---- + +/// What a far end answers a connection: given every byte it has read so far, the bytes to send +/// back and close (`Some`), or `None` to read on. +pub type Answer = fn(&[u8]) -> Option>; + +/// THE SUITE'S TLS FAR END at [`FAR_END`] (started once; later calls are no-ops): every connection's +/// handshake is busbar's test kit's, with a `localhost` certificate [`anchors`] trusts, and its +/// bytes are answered by `answer`. +/// +/// # Panics +/// [`FAR_END`] cannot be bound. +pub fn far_end(answer: Answer) { + static STARTED: OnceLock<()> = OnceLock::new(); + STARTED.get_or_init(|| { + let listener = TcpListener::bind(FAR_END) + .unwrap_or_else(|e| panic!("the suite's far end cannot bind {FAR_END}: {e}")); + serve(listener, answer); + }); +} + +/// Serve `listener` over TLS with a leaf the test CA signed, answering each connection by `answer`. +fn serve(listener: TcpListener, answer: Answer) { + let tls = ServerTls::new(&[ca().leaf_der.clone()], &ca().key_der, None, &[]) + .unwrap_or_else(|e| panic!("the suite's far end has no TLS identity: {e}")); + std::thread::spawn(move || { + for tcp in listener.incoming() { + let Ok(tcp) = tcp else { return }; + let tls = tls.clone(); + std::thread::spawn(move || { + if let Ok(mut s) = tls.accept_std(tcp) { + if s.handshake_ok() { + reply(&mut s, answer); + } + } + }); + } + }); +} + +fn reply(s: &mut StdServerSession, answer: Answer) { + let mut seen = Vec::new(); + let mut buf = [0_u8; 4096]; + loop { + if let Some(out) = answer(&seen) { + let _ = s.write_all(&out); + let _ = s.flush(); + s.close(); + return; + } + match s.read(&mut buf) { + Ok(0) | Err(_) => return, + Ok(n) => seen.extend_from_slice(&buf[..n]), + } + } +} + +/// A far end's cleartext negotiation before TLS (a protocol's own StartTLS: Postgres's +/// `SSLRequest` answered `S`, LDAP's StartTLS extended operation, ...): `true` = go on to TLS. +pub type Preamble = fn(&mut TcpStream) -> bool; + +/// THE SUITE'S TLS FRONT at [`FAR_END`] (started once; later calls are no-ops): each connection's +/// cleartext `preamble` runs, then the TLS handshake (busbar's test kit, a `localhost` certificate +/// [`anchors`] trusts), then its bytes are carried both ways to the REAL backend at `upstream` +/// (the plugin's live service, in the clear on loopback). What it proves: the plugin's own +/// connection is secured by the HOST's TLS against the suite's anchors. +/// +/// # Panics +/// [`FAR_END`] cannot be bound. +pub fn tls_front(preamble: Preamble, upstream: &'static str) { + static STARTED: OnceLock<()> = OnceLock::new(); + STARTED.get_or_init(|| { + let listener = TcpListener::bind(FAR_END) + .unwrap_or_else(|e| panic!("the suite's TLS front cannot bind {FAR_END}: {e}")); + let tls = ServerTls::new(&[ca().leaf_der.clone()], &ca().key_der, None, &[]) + .unwrap_or_else(|e| panic!("the suite's TLS front has no TLS identity: {e}")); + std::thread::spawn(move || { + for tcp in listener.incoming() { + let Ok(tcp) = tcp else { return }; + let tls = tls.clone(); + std::thread::spawn(move || front(tcp, &tls, preamble, upstream)); + } + }); + }); +} + +/// One connection through the TLS front: preamble, handshake, then both directions carried until +/// either side ends. +fn front(mut tcp: TcpStream, tls: &ServerTls, preamble: Preamble, upstream: &str) { + if !preamble(&mut tcp) { + return; + } + let Ok(ctl) = tcp.try_clone() else { return }; + let Ok(mut s) = tls.accept_std(tcp) else { + return; + }; + if !s.handshake_ok() { + return; + } + let Ok(mut up) = TcpStream::connect(upstream) else { + return; + }; + let tick = Some(Duration::from_millis(2)); + if ctl.set_read_timeout(tick).is_err() || up.set_read_timeout(tick).is_err() { + return; + } + let idle = |e: &std::io::Error| { + matches!( + e.kind(), + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut + ) + }; + let mut buf = [0_u8; 16 * 1024]; + loop { + match s.read(&mut buf) { + Ok(0) => return, + Ok(n) => { + if up.write_all(&buf[..n]).is_err() { + return; + } + } + Err(e) if idle(&e) => {} + Err(_) => return, + } + match up.read(&mut buf) { + Ok(0) => { + s.close(); + return; + } + Ok(n) => { + if s.write_all(&buf[..n]).and_then(|()| s.flush()).is_err() { + return; + } + } + Err(e) if idle(&e) => {} + Err(_) => return, + } + } +} + +// ---- the host's own proof, run with the suite ---- + +const OWNER: InstanceId = InstanceId(7); + +fn need(transport: &str, class: u32) -> ReadNeed { + ReadNeed { + direction: DIRECTION_OUTBOUND, + egress_class: class, + transport: transport.to_owned(), + auth: String::new(), + target_from: String::new(), + trust_from: String::new(), + details: ReadBlob { + fmt: 0, + flags: 0, + bytes: Vec::new(), + }, + timeout_ms: 0, + } +} + +/// `f` until it answers other than PENDING, as a plugin's re-entries on its ticket would. +fn settle(mut f: impl FnMut() -> Result) -> Result { + let until = Instant::now() + Duration::from_secs(10); + loop { + match f() { + Err(ConnError::Pending) if Instant::now() < until => { + std::thread::sleep(Duration::from_millis(2)); + } + answered => return answered, + } + } +} + +/// Read `id` to its completion (or `want` body bytes): its fields pieces' count and its body. +fn drain(c: &dyn DeclaredConns, id: ConnId, want: usize) -> (usize, Vec) { + let (mut fields, mut body) = (0, Vec::new()); + let mut buf = [0_u8; 256]; + while body.len() < want { + let Ok(p) = settle(|| c.read(OWNER, id, NO_TICKET, &mut buf)) else { + break; + }; + match p.kind { + PieceKind::Fields => fields += 1, + PieceKind::Body => body.extend_from_slice(&buf[..p.len]), + PieceKind::Completion => break, + PieceKind::HookReply => {} + } + } + (fields, body) +} + +/// Five bytes back for five bytes in: the TLS echo the host's own proof dials. +fn echo(seen: &[u8]) -> Option> { + (seen.len() >= 5).then(|| seen[..5].to_vec()) +} + +/// A raw `tcp` stream to a TLS echo, upgraded through the host's TLS: what came back. +fn upgraded_echo(c: &dyn DeclaredConns, far: &str) -> Result, ConnError> { + c.declare( + OWNER, + NeedId(0), + &need("tcp", EGRESS_OPERATOR_INFRASTRUCTURE), + None, + None, + )?; + let id = settle(|| { + c.open( + OWNER, + NeedId(0), + &OpenDesc { + target: far, + timeout_ms: 5_000, + ..OpenDesc::default() + }, + ) + })?; + settle(|| c.upgrade_secure(OWNER, id, Some("localhost"), None, false, NO_TICKET))?; + let mut at = 0; + while at < 5 { + at += settle(|| c.write(OWNER, id, &b"hello"[at..], false, false))?; + } + let (_, body) = drain(c, id, 5); + let _ = c.close(OWNER, id); + Ok(body) +} + +/// A TLS echo on a port of its own; its address. +fn tls_echo() -> String { + let listener = TcpListener::bind("127.0.0.1:0").expect("a local listener"); + let at = listener.local_addr().expect("its address").to_string(); + serve(listener, echo); + at +} + +/// THE HOST'S TLS TRUSTS THE SUITE'S TEST ANCHORS: a stream to a far end whose certificate chains +/// to the test CA is upgraded and round-trips (the anchors went to the host, never to the plugin). +#[test] +fn the_hosts_tls_upgrade_verifies_against_the_suites_test_anchors() { + let c = host(Arc::new(|_| {}), Some(anchors())); + assert_eq!( + upgraded_echo(c.as_ref(), &tls_echo()), + Ok(b"hello".to_vec()) + ); +} + +/// RED: the same upgrade with the anchors withheld is refused: the platform roots alone do not +/// trust the test CA. +#[test] +fn red_with_the_anchors_withheld_the_hosts_tls_upgrade_is_refused() { + let c = host(Arc::new(|_| {}), None); + let answer = upgraded_echo(c.as_ref(), &tls_echo()); + assert!(answer.is_err(), "upgraded over an untrusted CA: {answer:?}"); +} diff --git a/store-postgres/declares.json b/store-postgres/declares.json index 985cfd5..2b63b04 100644 --- a/store-postgres/declares.json +++ b/store-postgres/declares.json @@ -2,5 +2,8 @@ "contract_abi": { "min": 4, "max": 4 - } + }, + "needs": [ + "tcp" + ] }