├── .envrc ├── .gitignore ├── rustfmt.toml ├── src ├── tests.rs ├── service │ ├── auction_house │ │ └── xmpp.rs │ ├── bidding_engine │ │ └── postgres.rs │ ├── auction_house.rs │ ├── ui.rs │ └── bidding_engine.rs ├── progress.rs ├── auction.rs ├── event.rs ├── progress │ └── in_memory.rs ├── persistence │ ├── postgres.rs │ └── in_memory.rs ├── event_log.rs ├── main.rs ├── tests │ ├── event_log.rs │ └── bidding_engine.rs ├── persistence.rs ├── event_log │ └── in_memory.rs └── service.rs ├── shell.nix ├── default.nix ├── README.md ├── Cargo.toml ├── LICENSE ├── flake.nix ├── flake.lock └── Cargo.lock /.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity="Crate" 2 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | mod bidding_engine; 2 | mod event_log; 3 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let 4 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 5 | in 6 | fetchTarball { 7 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 8 | sha256 = lock.nodes.flake-compat.locked.narHash; 9 | } 10 | ) 11 | { 12 | src = ./.; 13 | }).shellNix 14 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let 4 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 5 | in 6 | fetchTarball { 7 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 8 | sha256 = lock.nodes.flake-compat.locked.narHash; 9 | } 10 | ) 11 | { 12 | src = ./.; 13 | }).defaultNix 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Auction Sniper 2 | 3 | Educational Rust not-OOP implemenation of Auction Sniper from "Growing Object-Oriented Software, Guided By Tests" book 4 | 5 | 6 | More about it in [Data-oriented, clean&hexagonal architecture software in Rust – through an example project](https://dpc.pw/data-oriented-cleanandhexagonal-architecture-software-in-rust-through-an-example) 7 | blog post. 8 | 9 | Features: 10 | 11 | * Services (main application logical threads/actors) with graceful shutdown on demand or error 12 | * Simple Event-Log-based communication. 13 | * Ports&Repositories from the Hexagonal Architecture with support for cross-repository database transactions 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sniper" 3 | version = "0.1.0" 4 | edition = "2021" 5 | rust-version = "1.68.2" 6 | license = "MIT" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | thiserror = "*" 12 | anyhow = "*" 13 | ctrlc = "*" 14 | parking_lot = "*" 15 | 16 | postgres = "*" 17 | r2d2 = "*" 18 | r2d2_postgres = "*" 19 | 20 | axum = "0.6" 21 | tokio = { version = "1.28", features = ["rt", "rt-multi-thread"] } 22 | async-trait = "*" 23 | futures = { version = "*", features = ["async-await"] } 24 | async-condvar-fair = { version = "*", features = ["tokio"] } 25 | tracing = "0.1" 26 | tracing-subscriber = "0.3" 27 | serde = { version = "*", features = ["derive"] } 28 | dyno = "*" 29 | -------------------------------------------------------------------------------- /src/service/auction_house/xmpp.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use tracing::debug; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct XmppAuctionHouseClient; 6 | 7 | impl XmppAuctionHouseClient { 8 | pub fn new() -> Self { 9 | Self 10 | } 11 | 12 | pub fn new_shared() -> SharedAuctionHouseClient { 13 | Arc::new(Self::new()) 14 | } 15 | } 16 | 17 | impl AuctionHouseClient for XmppAuctionHouseClient { 18 | fn place_bid(&self, item_id: ItemIdRef, price: Amount) -> Result<()> { 19 | debug!(?item_id, ?price, "sending bid"); 20 | todo!() 21 | } 22 | 23 | fn poll(&self, timeout: Option) -> Result> { 24 | timeout.map(|t| std::thread::sleep(t)); 25 | // TODO 26 | Ok(None) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/progress.rs: -------------------------------------------------------------------------------- 1 | mod in_memory; 2 | 3 | pub use self::in_memory::*; 4 | 5 | use crate::{ 6 | event_log::Offset, 7 | persistence::{Connection, Transaction}, 8 | service::{ServiceId, ServiceIdRef}, 9 | }; 10 | use anyhow::format_err; 11 | use std::sync::Arc; 12 | 13 | use anyhow::Result; 14 | 15 | /// A persistent store to keep track of the last processed event 16 | pub trait ProgressTracker { 17 | fn load(&self, conn: &mut dyn Connection, id: ServiceIdRef) -> Result>; 18 | 19 | fn store_tr( 20 | &self, 21 | conn: &mut dyn Transaction<'_>, 22 | id: ServiceIdRef, 23 | offset: Offset, 24 | ) -> Result<()>; 25 | fn load_tr(&self, conn: &mut dyn Transaction<'_>, id: ServiceIdRef) -> Result>; 26 | } 27 | 28 | pub type SharedProgressTracker = Arc; 29 | -------------------------------------------------------------------------------- /src/auction.rs: -------------------------------------------------------------------------------- 1 | pub type ItemId = String; 2 | pub type ItemIdRef<'s> = &'s str; 3 | pub type Amount = u64; 4 | 5 | #[derive(Copy, Clone, PartialEq, Eq, Debug)] 6 | pub enum Bidder { 7 | Sniper, 8 | #[allow(unused)] 9 | Other, 10 | } 11 | 12 | #[derive(Clone, PartialEq, Eq)] 13 | pub struct Bid { 14 | pub item: ItemId, 15 | pub details: BidDetails, 16 | } 17 | 18 | #[derive(Clone, PartialEq, Eq, Debug)] 19 | pub struct ItemBid { 20 | pub item: ItemId, 21 | pub price: Amount, 22 | } 23 | 24 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 25 | pub struct BidDetails { 26 | pub bidder: Bidder, 27 | pub price: Amount, 28 | pub increment: Amount, 29 | } 30 | 31 | impl BidDetails { 32 | pub fn next_valid_bid(self) -> Amount { 33 | self.price + self.increment 34 | } 35 | 36 | pub fn is_outbidded_by(self, other: Amount) -> bool { 37 | self.next_valid_bid() <= other 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Dawid Ciężarkiewicz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use crate::auction::*; 2 | use thiserror::Error; 3 | 4 | // TODO: This type makes everything cyclical: 5 | // All services depend on it, and it depends 6 | // on events of each of the services. Not a 7 | // big deal for this small program, but something 8 | // to take care of in a more realistic implementation. 9 | #[derive(Clone, Debug, PartialEq, Eq)] 10 | pub enum Event { 11 | AuctionHouse(AuctionHouseEvent), 12 | BiddingEngine(BiddingEngineEvent), 13 | Ui(UiEvent), 14 | #[cfg(test)] 15 | Test, 16 | } 17 | 18 | #[derive(Clone, Debug, PartialEq, Eq)] 19 | pub struct AuctionHouseEvent { 20 | pub item: ItemId, 21 | pub event: AuctionHouseItemEvent, 22 | } 23 | 24 | #[derive(Clone, Debug, PartialEq, Eq)] 25 | pub enum AuctionHouseItemEvent { 26 | Bid(BidDetails), 27 | Closed, 28 | } 29 | 30 | #[derive(Clone, Debug, PartialEq, Eq)] 31 | pub enum BiddingEngineEvent { 32 | /// We are placing a bid 33 | Bid(ItemBid), 34 | /// Auction house event caused an error 35 | AuctionError(BiddingEngineAuctionError), 36 | /// User event caused an error 37 | UserError(BiddingEngineUserError), 38 | } 39 | 40 | #[derive(Error, Debug, Copy, Clone, PartialEq, Eq)] 41 | pub enum BiddingEngineUserError { 42 | #[error("auction already closed")] 43 | AlreadyClosed, 44 | #[error("bid is too low")] 45 | TooLow, 46 | } 47 | 48 | #[derive(Error, Clone, Debug, PartialEq, Eq)] 49 | pub enum BiddingEngineAuctionError { 50 | #[error("unknown auction: {0}")] 51 | UnknownAuction(ItemId), 52 | } 53 | #[derive(Clone, Debug, PartialEq, Eq)] 54 | pub enum UiEvent { 55 | MaxBidSet(ItemBid), 56 | } 57 | -------------------------------------------------------------------------------- /src/progress/in_memory.rs: -------------------------------------------------------------------------------- 1 | use crate::persistence::{InMemoryConnection, InMemoryTransaction}; 2 | use anyhow::Result; 3 | use std::{ 4 | collections::BTreeMap, 5 | sync::{Arc, Mutex, MutexGuard}, 6 | }; 7 | 8 | use super::*; 9 | 10 | pub struct InMemoryProgressTracker { 11 | store: Mutex>, 12 | } 13 | 14 | impl InMemoryProgressTracker { 15 | pub fn new() -> Self { 16 | Self { 17 | store: Mutex::new(BTreeMap::default()), 18 | } 19 | } 20 | 21 | pub fn new_shared() -> SharedProgressTracker { 22 | Arc::new(Self::new()) 23 | } 24 | 25 | pub fn lock(&self) -> Result>> { 26 | self.store 27 | .lock() 28 | .map_err(|_e| format_err!("mutex poisoned")) 29 | } 30 | } 31 | 32 | impl ProgressTracker for InMemoryProgressTracker { 33 | fn load<'a>(&self, conn: &mut dyn Connection, id: ServiceIdRef) -> Result> { 34 | conn.cast().as_mut::()?; 35 | Ok(self.lock()?.get(id).cloned()) 36 | } 37 | 38 | fn store_tr<'a>( 39 | &self, 40 | conn: &mut dyn Transaction, 41 | id: ServiceIdRef, 42 | event_id: Offset, 43 | ) -> Result<()> { 44 | conn.cast().as_mut::()?; 45 | 46 | self.lock()?.insert(id.to_owned(), event_id.to_owned()); 47 | Ok(()) 48 | } 49 | 50 | fn load_tr<'a>(&self, conn: &mut dyn Transaction, id: ServiceIdRef) -> Result> { 51 | conn.cast().as_mut::()?; 52 | Ok(self.lock()?.get(id).cloned()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Auction Sniper in Rust"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | 8 | flake-compat = { 9 | url = "github:edolstra/flake-compat"; 10 | flake = false; 11 | }; 12 | 13 | fenix = { 14 | url = "github:nix-community/fenix"; 15 | inputs.nixpkgs.follows = "nixpkgs"; 16 | }; 17 | 18 | naersk = { 19 | url = "github:nix-community/naersk"; 20 | inputs.nixpkgs.follows = "nixpkgs"; 21 | }; 22 | }; 23 | 24 | outputs = { self, naersk, nixpkgs, flake-utils, flake-compat, fenix }: 25 | flake-utils.lib.eachDefaultSystem (system: 26 | let 27 | pkgs = nixpkgs.legacyPackages."${system}"; 28 | fenix-pkgs = fenix.packages.${system}; 29 | fenix-channel = fenix-pkgs.complete; 30 | naersk-lib = naersk.lib."${system}".override { 31 | inherit (fenix-pkgs.minimal) cargo rustc; 32 | }; 33 | in rec { 34 | packages.sniper = naersk-lib.buildPackage ./.; 35 | 36 | defaultPackage = self.packages.${system}.sniper; 37 | defaultApp = self.packages.${system}.sniper; 38 | 39 | # `nix develop` 40 | devShell = pkgs.mkShell 41 | { 42 | inputsFrom = builtins.attrValues self.packages.${system}; 43 | buildInputs = [ pkgs.libsodium pkgs.lzma pkgs.openssl ]; 44 | nativeBuildInputs = (with pkgs; 45 | [ 46 | pkgconfig 47 | fenix-pkgs.rust-analyzer 48 | fenix-channel.rustfmt 49 | fenix-channel.rustc 50 | ]); 51 | RUST_SRC_PATH = "${fenix-channel.rust-src}/lib/rustlib/src/rust/library"; 52 | }; 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /src/persistence/postgres.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct PostgresPersistence { 5 | pool: r2d2::Pool>, 6 | } 7 | 8 | impl Persistence for PostgresPersistence { 9 | fn get_connection(&self) -> Result> { 10 | Ok(Box::new(PostgresConnection(self.pool.get()?))) 11 | } 12 | } 13 | 14 | pub struct PostgresConnection( 15 | pub r2d2::PooledConnection< 16 | r2d2_postgres::PostgresConnectionManager, 17 | >, 18 | ); 19 | 20 | impl<'a> dyno::Tag<'a> for PostgresConnection { 21 | type Type = PostgresConnection; 22 | } 23 | impl Connection for PostgresConnection { 24 | fn start_transaction<'a>(&'a mut self) -> Result + 'a>> { 25 | Ok(Box::new(PostgresTransaction(self.0.transaction()?))) 26 | } 27 | 28 | fn cast(&mut self) -> Caster<'_, 'static> { 29 | Caster::new::(self) 30 | } 31 | } 32 | 33 | pub struct PostgresTransaction<'a>(pub ::postgres::Transaction<'a>); 34 | 35 | impl<'a> dyno::Tag<'a> for PostgresTransaction<'static> { 36 | type Type = PostgresTransaction<'a>; 37 | } 38 | 39 | impl<'a> Transaction<'a> for PostgresTransaction<'a> { 40 | fn commit(self: Box) -> Result<()> { 41 | Ok(((self.0) as ::postgres::Transaction<'a>).commit()?) 42 | } 43 | 44 | fn rollback(self: Box) -> Result<()> { 45 | Ok(((self.0) as ::postgres::Transaction<'a>).rollback()?) 46 | } 47 | 48 | fn cast<'caster>(&'caster mut self) -> Caster<'caster, 'a> 49 | where 50 | 'a: 'caster, 51 | { 52 | Caster::new::>(self) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/event_log.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | event::Event, 3 | persistence::{Connection, Transaction}, 4 | }; 5 | use anyhow::{format_err, Result}; 6 | use std::{convert::TryFrom, sync::Arc, time::Duration}; 7 | 8 | mod in_memory; 9 | pub use self::in_memory::*; 10 | 11 | pub type Offset = u64; 12 | 13 | #[derive(Clone, Debug, PartialEq, Eq)] 14 | pub struct LogEvent { 15 | pub offset: Offset, 16 | pub details: Event, 17 | } 18 | 19 | #[derive(Clone, Debug, PartialEq, Eq)] 20 | pub struct WithOffset { 21 | pub offset: Offset, 22 | pub data: T, 23 | } 24 | 25 | pub trait Reader { 26 | fn get_start_offset(&self) -> Result; 27 | 28 | fn read( 29 | &self, 30 | conn: &mut dyn Connection, 31 | offset: Offset, 32 | limit: usize, 33 | timeout: Option, 34 | ) -> Result>>; 35 | 36 | fn read_one( 37 | &self, 38 | conn: &mut dyn Connection, 39 | offset: Offset, 40 | ) -> Result>> { 41 | let WithOffset { offset, data } = 42 | self.read(conn, offset, 1, Some(Duration::from_millis(0)))?; 43 | assert!(data.len() <= 1); 44 | Ok(WithOffset { 45 | offset, 46 | data: data.into_iter().next(), 47 | }) 48 | } 49 | } 50 | 51 | pub trait Writer { 52 | fn write(&self, conn: &mut dyn Connection, events: &[Event]) -> Result { 53 | self.write_tr(&mut *conn.start_transaction()?, events) 54 | } 55 | 56 | fn write_tr(&self, conn: &mut dyn Transaction<'_>, events: &[Event]) -> Result; 57 | } 58 | 59 | pub type SharedReader = Arc; 60 | pub type SharedWriter = Arc; 61 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod auction; 2 | mod event; 3 | mod event_log; 4 | mod persistence; 5 | mod progress; 6 | mod service; 7 | 8 | use anyhow::Result; 9 | use std::sync::Arc; 10 | 11 | fn main() -> Result<()> { 12 | tracing_subscriber::fmt::init(); 13 | 14 | let persistence = Arc::new(persistence::InMemoryPersistence::new()); 15 | let progress_store = progress::InMemoryProgressTracker::new_shared(); 16 | let (event_writer, event_reader) = event_log::new_in_memory_shared()?; 17 | let auction_house_client = service::auction_house::XmppAuctionHouseClient::new_shared(); 18 | 19 | let svc_ctr = service::ServiceControl::new(persistence.clone(), progress_store); 20 | 21 | ctrlc::set_handler({ 22 | let svc_ctr = svc_ctr.clone(); 23 | move || { 24 | eprintln!("Stopping all services..."); 25 | svc_ctr.send_stop_to_all(); 26 | } 27 | })?; 28 | 29 | let bidding_state_store = service::InMemoryBiddingStateStore::new_shared(); 30 | for handle in vec![ 31 | svc_ctr.spawn_log_follower( 32 | service::bidding_engine::BiddingEngine::new(bidding_state_store, event_writer.clone()), 33 | event_reader.clone(), 34 | ), 35 | svc_ctr.spawn_loop(service::AuctionHouseReceiver::new( 36 | persistence.clone(), 37 | event_writer.clone(), 38 | auction_house_client.clone(), 39 | )), 40 | svc_ctr.spawn_log_follower( 41 | service::AuctionHouseSender::new(auction_house_client.clone()), 42 | event_reader.clone(), 43 | ), 44 | svc_ctr.spawn_loop(service::Ui::new(persistence, event_writer.clone())?), 45 | ] { 46 | handle.join()? 47 | } 48 | 49 | Ok(()) 50 | } 51 | 52 | #[cfg(test)] 53 | mod tests; 54 | -------------------------------------------------------------------------------- /src/tests/event_log.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::{ 4 | event::*, 5 | event_log::{self, LogEvent, WithOffset}, 6 | persistence::{self, Persistence}, 7 | }; 8 | use anyhow::Result; 9 | 10 | #[test] 11 | fn event_logs_sanity_check() -> Result<()> { 12 | let persistence = persistence::InMemoryPersistence::new(); 13 | let (event_writer, event_reader) = event_log::new_in_memory_shared()?; 14 | 15 | let start_offset = event_reader.get_start_offset()?; 16 | 17 | let mut conn = persistence.get_connection()?; 18 | 19 | assert_eq!( 20 | event_reader.read(&mut *conn, start_offset, 0, Some(Duration::from_secs(0)))?, 21 | WithOffset { 22 | offset: start_offset, 23 | data: vec![] 24 | } 25 | ); 26 | 27 | assert_eq!( 28 | event_reader.read(&mut *conn, start_offset, 1, Some(Duration::from_secs(0)))?, 29 | WithOffset { 30 | offset: start_offset, 31 | data: vec![] 32 | } 33 | ); 34 | 35 | let inserted_offset = event_writer.write(&mut *conn, &[Event::Test])?; 36 | 37 | assert_eq!( 38 | event_reader.read(&mut *conn, inserted_offset, 1, Some(Duration::from_secs(0)))?, 39 | WithOffset { 40 | offset: inserted_offset, 41 | data: vec![] 42 | } 43 | ); 44 | 45 | assert_eq!( 46 | event_reader.read( 47 | &mut *conn, 48 | event_reader.get_start_offset()?, 49 | 1, 50 | Some(Duration::from_secs(0)) 51 | )?, 52 | WithOffset { 53 | offset: inserted_offset, 54 | data: vec![LogEvent { 55 | offset: event_reader.get_start_offset()?, 56 | details: Event::Test 57 | }] 58 | } 59 | ); 60 | 61 | Ok(()) 62 | } 63 | -------------------------------------------------------------------------------- /src/persistence/in_memory.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use futures; 3 | use tokio::sync::{Mutex, MutexGuard}; 4 | 5 | /// Fake in-memory persistence. 6 | /// 7 | /// Useful for unit-tests. 8 | #[derive(Debug, Clone)] 9 | pub struct InMemoryPersistence { 10 | lock: Arc>, 11 | } 12 | 13 | impl InMemoryPersistence { 14 | pub fn new() -> Self { 15 | Self { 16 | lock: Arc::new(Mutex::new(())), 17 | } 18 | } 19 | } 20 | 21 | impl Persistence for InMemoryPersistence { 22 | fn get_connection(&self) -> Result> { 23 | Ok(Box::new(InMemoryConnection { 24 | lock: self.lock.clone(), 25 | })) 26 | } 27 | } 28 | 29 | #[derive(Default, Debug)] 30 | pub struct InMemoryConnection { 31 | lock: Arc>, 32 | } 33 | 34 | impl<'a> dyno::Tag<'a> for InMemoryConnection { 35 | type Type = InMemoryConnection; 36 | } 37 | 38 | impl Connection for InMemoryConnection { 39 | fn start_transaction(&mut self) -> Result> { 40 | Ok(Box::new(InMemoryTransaction { 41 | _lock_guard: futures::executor::block_on(self.lock.lock()), 42 | })) 43 | } 44 | 45 | fn cast(&mut self) -> Caster<'_, 'static> { 46 | Caster::new::(self) 47 | } 48 | } 49 | 50 | #[derive(Debug)] 51 | pub struct InMemoryTransaction<'a> { 52 | _lock_guard: MutexGuard<'a, ()>, 53 | } 54 | 55 | impl<'a> dyno::Tag<'a> for InMemoryTransaction<'static> { 56 | type Type = InMemoryTransaction<'a>; 57 | } 58 | 59 | impl<'a> Transaction<'a> for InMemoryTransaction<'a> { 60 | fn commit(self: Box) -> Result<()> { 61 | Ok(()) 62 | } 63 | 64 | // TODO: simulating rollbacks in a general way is not trivial 65 | // and it would require all the `InMemory*` stores implementations 66 | // to register previous value when creating the transaction or 67 | // something like this. 68 | fn rollback(self: Box) -> Result<()> { 69 | bail!("Not supported") 70 | } 71 | 72 | fn cast<'caster>(&'caster mut self) -> Caster<'caster, 'a> 73 | where 74 | 'a: 'caster, 75 | { 76 | Caster::new::>(self) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/service/bidding_engine/postgres.rs: -------------------------------------------------------------------------------- 1 | use crate::persistence::{ 2 | postgres::PostgresTransaction, Connection, PostgresConnection, Transaction, 3 | }; 4 | use anyhow::Result; 5 | use std::convert::TryFrom; 6 | 7 | pub struct PostgresBiddingStateStore { 8 | client: postgres::Client, 9 | } 10 | 11 | impl super::BiddingStateStore for PostgresBiddingStateStore { 12 | #[allow(unreachable_code)] 13 | fn load_tr( 14 | &self, 15 | conn: &mut dyn Transaction, 16 | item_id: crate::auction::ItemIdRef, 17 | ) -> anyhow::Result> { 18 | conn.cast().as_mut::()?.0.query_opt("SELECT max_bid_limit, last_bid_sent, higest_bid_bidder, higest_bid_price, highest_bid_increment, closed FROM bidding_state WHERE item_id = $0", &[&item_id])? 19 | .map::, _>(|row| { 20 | Ok(super::AuctionBiddingState { 21 | max_bid_limit: u64::try_from(row.get::<'_, _, i64>("max_bid_limit"))?, 22 | last_bid_sent: row.get::<'_,_, Option>("last_bid_sent").map(u64::try_from).transpose()?, 23 | auction_state: super::AuctionState { 24 | closed: row.get("closed"), 25 | higest_bid: todo!(), 26 | } 27 | }) 28 | }).transpose() 29 | } 30 | 31 | #[allow(unreachable_code)] 32 | fn load( 33 | &self, 34 | conn: &mut dyn Connection, 35 | item_id: crate::auction::ItemIdRef, 36 | ) -> anyhow::Result> { 37 | conn.cast().as_mut::()?.0.query_opt("SELECT max_bid_limit, last_bid_sent, higest_bid_bidder, higest_bid_price, highest_bid_increment, closed FROM bidding_state WHERE item_id = $0", &[&item_id])? 38 | .map::, _>(|row| { 39 | Ok(super::AuctionBiddingState { 40 | max_bid_limit: u64::try_from(row.get::<'_, _, i64>("max_bid_limit"))?, 41 | last_bid_sent: row.get::<'_,_, Option>("last_bid_sent").map(u64::try_from).transpose()?, 42 | auction_state: super::AuctionState { 43 | closed: row.get("closed"), 44 | higest_bid: todo!(), 45 | } 46 | }) 47 | }).transpose() 48 | } 49 | fn store_tr( 50 | &self, 51 | _conn: &mut dyn Transaction, 52 | _item_id: crate::auction::ItemIdRef, 53 | _state: super::AuctionBiddingState, 54 | ) -> anyhow::Result<()> { 55 | todo!() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/service/auction_house.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::Arc, time::Duration}; 2 | 3 | use crate::{ 4 | auction::{Amount, ItemIdRef}, 5 | event::{AuctionHouseEvent, BiddingEngineEvent, Event}, 6 | event_log, 7 | }; 8 | use anyhow::Result; 9 | use tracing::debug; 10 | 11 | use super::*; 12 | 13 | mod xmpp; 14 | pub use self::xmpp::*; 15 | 16 | pub trait AuctionHouseClient { 17 | fn place_bid(&self, item_id: ItemIdRef, price: Amount) -> Result<()>; 18 | fn poll(&self, timeout: Option) -> Result>; 19 | } 20 | 21 | pub type SharedAuctionHouseClient = Arc; 22 | 23 | pub struct AuctionHouseSender { 24 | auction_house_client: SharedAuctionHouseClient, 25 | } 26 | 27 | impl AuctionHouseSender { 28 | pub fn new(auction_house_client: SharedAuctionHouseClient) -> Self { 29 | Self { 30 | auction_house_client, 31 | } 32 | } 33 | } 34 | 35 | impl LogFollowerService for AuctionHouseSender { 36 | fn get_log_progress_id(&self) -> String { 37 | "auction-house-sender".to_owned() 38 | } 39 | 40 | fn handle_event(&mut self, _transaction: &mut dyn Transaction<'_>, event: Event) -> Result<()> { 41 | debug!(?event, "event"); 42 | match event { 43 | Event::BiddingEngine(BiddingEngineEvent::Bid(item_bid)) => { 44 | // Note: we rely on idempotency of this call to the server here 45 | self.auction_house_client 46 | .place_bid(&item_bid.item, item_bid.price) 47 | } 48 | _ => Ok(()), 49 | } 50 | } 51 | } 52 | 53 | pub struct AuctionHouseReceiver { 54 | persistence: SharedPersistence, 55 | even_writer: event_log::SharedWriter, 56 | auction_house_client: SharedAuctionHouseClient, 57 | } 58 | 59 | impl AuctionHouseReceiver { 60 | pub fn new( 61 | persistence: SharedPersistence, 62 | even_writer: event_log::SharedWriter, 63 | auction_house_client: SharedAuctionHouseClient, 64 | ) -> Self { 65 | Self { 66 | persistence, 67 | auction_house_client, 68 | even_writer, 69 | } 70 | } 71 | } 72 | 73 | impl LoopService for AuctionHouseReceiver { 74 | fn run_iteration<'a>(&mut self) -> Result<()> { 75 | // TODO: no atomicity offered by the auction_house_client interface 76 | if let Some(event) = self 77 | .auction_house_client 78 | .poll(Some(Duration::from_secs(1)))? 79 | { 80 | let mut connection = self.persistence.get_connection()?; 81 | self.even_writer 82 | .write(&mut *connection, &[Event::AuctionHouse(event)])?; 83 | } 84 | 85 | Ok(()) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/persistence.rs: -------------------------------------------------------------------------------- 1 | //! Database persistence traits 2 | //! 3 | //! OK, so this one is complex. Expressing atomic transactions 4 | //! spaning accross multiple stores/repositories in a hexagonal 5 | //! architecture is not a simple thing in any programming language. 6 | //! 7 | //! Some discussion: 8 | //! 9 | //! * https://www.reddit.com/r/rust/comments/p9amqt/hexagonal_architecture_in_rust_1/h9ypjoo?utm_source=share&utm_medium=web2x&context=3 10 | //! * https://www.reddit.com/r/golang/comments/i1vy4s/ddd_vs_db_transactions_how_to_reconcile/ 11 | pub mod in_memory; 12 | pub mod postgres; 13 | 14 | pub use self::{in_memory::*, postgres::*}; 15 | use thiserror::Error; 16 | 17 | use dyno::{Tag, Tagged}; 18 | 19 | use anyhow::{bail, Result}; 20 | use std::{any::Any, sync::Arc}; 21 | 22 | /// An interface of any persistence 23 | /// 24 | /// Persistence is anything that a Repository implementation could 25 | /// use to store data. 26 | pub trait Persistence: Send + Sync { 27 | /// Get a connection to persistence 28 | fn get_connection(&self) -> Result; 29 | } 30 | 31 | pub type SharedPersistence = Arc; 32 | 33 | pub trait Connection: Any { 34 | fn start_transaction(&mut self) -> Result>; 35 | 36 | fn cast(&mut self) -> Caster<'_, 'static>; 37 | } 38 | 39 | pub type OwnedConnection = Box; 40 | 41 | pub trait Transaction<'a> { 42 | fn commit(self: Box) -> Result<()>; 43 | fn rollback(self: Box) -> Result<()>; 44 | 45 | fn cast<'b>(&'b mut self) -> Caster<'b, 'a> 46 | where 47 | 'a: 'b; 48 | } 49 | 50 | pub type OwnedTransaction<'a> = Box + 'a>; 51 | 52 | #[derive(Error, Debug)] 53 | pub enum Error { 54 | #[error("wrong type")] 55 | WrongType, 56 | } 57 | 58 | /// Dynamic cast helper 59 | /// 60 | /// This struct allows an implementation of a Repository 61 | /// to cast at runtime a type-erased [`Transaction`] or [`Connection`] 62 | /// instance to back to a concrete type that it needs and expects. 63 | /// 64 | /// # Safety 65 | /// See https://users.rust-lang.org/t/help-with-using-any-to-cast-t-a-back-and-forth/69900/8 66 | /// 67 | /// The safety is enforced by the fact that `Caster` pinky-promises to never 68 | /// allow any reference other that `&'caster mut T` out of itself, and 69 | /// `'a` must always outlive `'caster` or borrowck will be upset. 70 | pub struct Caster<'borrow, 'value>(&'borrow mut (dyn Tagged<'value> + 'value)); 71 | 72 | impl<'borrow, 'value> Caster<'borrow, 'value> { 73 | pub fn new>(any: &'borrow mut I::Type) -> Self { 74 | Self(::tag_mut::(any)) 75 | } 76 | 77 | // Returns `Result` so it's easier to handle with ? than an option 78 | pub fn as_mut>(&'borrow mut self) -> Result<&'borrow mut I::Type, Error> { 79 | self.0.downcast_mut::().ok_or(Error::WrongType) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fenix": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ], 8 | "rust-analyzer-src": "rust-analyzer-src" 9 | }, 10 | "locked": { 11 | "lastModified": 1631672792, 12 | "narHash": "sha256-mKOrm7Dvvpi/bNPl196SwDSTpGWs6CGB8jsMT1/MLu0=", 13 | "owner": "nix-community", 14 | "repo": "fenix", 15 | "rev": "e917dae2552d6d50797a718bffba34102af082bf", 16 | "type": "github" 17 | }, 18 | "original": { 19 | "owner": "nix-community", 20 | "repo": "fenix", 21 | "type": "github" 22 | } 23 | }, 24 | "flake-compat": { 25 | "flake": false, 26 | "locked": { 27 | "lastModified": 1627913399, 28 | "narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=", 29 | "owner": "edolstra", 30 | "repo": "flake-compat", 31 | "rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2", 32 | "type": "github" 33 | }, 34 | "original": { 35 | "owner": "edolstra", 36 | "repo": "flake-compat", 37 | "type": "github" 38 | } 39 | }, 40 | "flake-utils": { 41 | "locked": { 42 | "lastModified": 1631561581, 43 | "narHash": "sha256-3VQMV5zvxaVLvqqUrNz3iJelLw30mIVSfZmAaauM3dA=", 44 | "owner": "numtide", 45 | "repo": "flake-utils", 46 | "rev": "7e5bf3925f6fbdfaf50a2a7ca0be2879c4261d19", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "numtide", 51 | "repo": "flake-utils", 52 | "type": "github" 53 | } 54 | }, 55 | "naersk": { 56 | "inputs": { 57 | "nixpkgs": [ 58 | "nixpkgs" 59 | ] 60 | }, 61 | "locked": { 62 | "lastModified": 1631004250, 63 | "narHash": "sha256-LGh0CjAZwh13AVkTi9w9lITEC7x6bwSQyFViOZ6HyNo=", 64 | "owner": "nix-community", 65 | "repo": "naersk", 66 | "rev": "08afb3d1dbfe016108b72e05b02ba0f6ecb3c8e1", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "nix-community", 71 | "repo": "naersk", 72 | "type": "github" 73 | } 74 | }, 75 | "nixpkgs": { 76 | "locked": { 77 | "lastModified": 1631676318, 78 | "narHash": "sha256-F7B0ZN/EgCX/8auBjGzfWHcQjHD7YB8TtHiD7laORD4=", 79 | "owner": "nixos", 80 | "repo": "nixpkgs", 81 | "rev": "d91cd9654f7b14115001bf4a920bd03593d530e6", 82 | "type": "github" 83 | }, 84 | "original": { 85 | "owner": "nixos", 86 | "repo": "nixpkgs", 87 | "type": "github" 88 | } 89 | }, 90 | "root": { 91 | "inputs": { 92 | "fenix": "fenix", 93 | "flake-compat": "flake-compat", 94 | "flake-utils": "flake-utils", 95 | "naersk": "naersk", 96 | "nixpkgs": "nixpkgs" 97 | } 98 | }, 99 | "rust-analyzer-src": { 100 | "flake": false, 101 | "locked": { 102 | "lastModified": 1631644319, 103 | "narHash": "sha256-Q/d+KLvVURRuz7pFv+TZ+3QCL+s4VmGub8XCdOUkr0M=", 104 | "owner": "rust-analyzer", 105 | "repo": "rust-analyzer", 106 | "rev": "726a2aa211974a6bb1a612fe8824df7baf59faf6", 107 | "type": "github" 108 | }, 109 | "original": { 110 | "owner": "rust-analyzer", 111 | "ref": "nightly", 112 | "repo": "rust-analyzer", 113 | "type": "github" 114 | } 115 | } 116 | }, 117 | "root": "root", 118 | "version": 7 119 | } 120 | -------------------------------------------------------------------------------- /src/event_log/in_memory.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::{event::Event, persistence::InMemoryTransaction}; 3 | use async_condvar_fair::Condvar; 4 | use futures::FutureExt; 5 | use tokio::sync::{OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock}; 6 | 7 | type InMemoryLogInner = Vec; 8 | 9 | pub struct InMemoryLog { 10 | inner: Arc>, 11 | condvar: Arc, 12 | runtime: tokio::runtime::Runtime, 13 | } 14 | 15 | impl InMemoryLog { 16 | pub async fn read(&self) -> OwnedRwLockReadGuard { 17 | self.inner.clone().read_owned().await 18 | } 19 | 20 | pub async fn write(&self) -> OwnedRwLockWriteGuard { 21 | self.inner.clone().write_owned().await 22 | } 23 | 24 | async fn write_events(&self, events: &[Event]) -> Result { 25 | let mut write = self.write().await; 26 | 27 | write.extend_from_slice(events); 28 | self.condvar.notify_all(); 29 | 30 | Ok(u64::try_from(write.len())?) 31 | } 32 | } 33 | 34 | impl Reader for InMemoryLog { 35 | fn read( 36 | &self, 37 | _conn: &mut dyn Connection, 38 | offset: Offset, 39 | limit: usize, 40 | timeout: Option, 41 | ) -> Result>> { 42 | let offset_usize = usize::try_from(offset)?; 43 | let condvar = self.condvar.clone(); 44 | 45 | let read = self.runtime.block_on(async { 46 | let read = self.read().await; 47 | 48 | if read.len() == offset_usize { 49 | if let Some(timeout) = timeout { 50 | let timeout_fut = async move { 51 | tokio::time::sleep(timeout).await; 52 | condvar.notify_all(); 53 | } 54 | .fuse(); 55 | let wait_fut = self.condvar.wait((read, self.inner.clone())).fuse(); 56 | let mut timeout_fut = Box::pin(timeout_fut); 57 | let mut wait_fut = Box::pin(wait_fut); 58 | 59 | loop { 60 | futures::select! { 61 | read = wait_fut => break read, 62 | _ = timeout_fut => {}, 63 | }; 64 | } 65 | } else { 66 | self.condvar.wait((read, self.inner.clone())).await 67 | } 68 | } else { 69 | read 70 | } 71 | }); 72 | 73 | let res: Vec<_> = read 74 | .get(offset_usize..) 75 | .ok_or_else(|| format_err!("out of bounds"))? 76 | .iter() 77 | .take(limit) 78 | .enumerate() 79 | .map(|(i, e)| LogEvent { 80 | offset: offset + u64::try_from(i).expect("no fail"), 81 | details: e.clone(), 82 | }) 83 | .collect(); 84 | 85 | Ok(WithOffset { 86 | offset: offset + u64::try_from(res.len()).expect("no fail"), 87 | data: res, 88 | }) 89 | } 90 | 91 | fn get_start_offset(&self) -> Result { 92 | Ok(0) 93 | } 94 | } 95 | 96 | impl Writer for InMemoryLog { 97 | fn write_tr<'a>(&self, conn: &mut dyn Transaction, events: &[Event]) -> Result { 98 | conn.cast().as_mut::()?; 99 | futures::executor::block_on(self.write_events(events)) 100 | } 101 | } 102 | 103 | pub fn new_in_memory_shared() -> Result<(SharedWriter, SharedReader)> { 104 | let log = Arc::new(InMemoryLog { 105 | inner: Arc::new(RwLock::new(Vec::new())), 106 | condvar: Arc::new(Condvar::default()), 107 | runtime: tokio::runtime::Runtime::new()?, 108 | }); 109 | Ok((log.clone(), log)) 110 | } 111 | -------------------------------------------------------------------------------- /src/service/ui.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | auction::{Amount, ItemBid}, 3 | event, event_log, 4 | persistence::SharedPersistence, 5 | service::LoopService, 6 | }; 7 | use anyhow::{format_err, Context, Result}; 8 | use axum::{ 9 | http::StatusCode, 10 | routing::{get, post}, 11 | Json, Router, 12 | }; 13 | use serde::Deserialize; 14 | use tokio::{runtime::Runtime, sync::oneshot}; 15 | 16 | pub struct Ui { 17 | // cancels all tasks on drop 18 | _runtime: Runtime, 19 | server_rx: oneshot::Receiver>, 20 | } 21 | 22 | #[derive(Deserialize)] 23 | struct BidRequest { 24 | item: String, 25 | price: Amount, 26 | } 27 | 28 | async fn handle_bid_request( 29 | persistence: SharedPersistence, 30 | even_writer: event_log::SharedWriter, 31 | bid_request: BidRequest, 32 | ) -> Result<()> { 33 | // OK, so here's the deal; mixing sync & async 34 | // code is a PITA and I don't want to convert 35 | // the whole project into async, at least ATM. 36 | // For mixing, one could define new set of traits 37 | // with async methods, and that works OKish, 38 | // though I've hit a problem of not being able 39 | // to share a [tokio::sync::Mutex] between 40 | // sync & async code in [crate::persistence::InMemoryPersistence]. 41 | // 42 | // Using `spawn_blocking` is lazy and should work, so I 43 | // leave it at that. 44 | tokio::task::spawn_blocking(move || { 45 | even_writer.write( 46 | &mut *persistence.get_connection()?, 47 | &[event::Event::Ui(event::UiEvent::MaxBidSet(ItemBid { 48 | item: bid_request.item, 49 | price: bid_request.price, 50 | }))], 51 | ) 52 | }) 53 | .await??; 54 | Ok(()) 55 | } 56 | 57 | async fn run_http_server( 58 | persistence: SharedPersistence, 59 | even_writer: event_log::SharedWriter, 60 | ) -> Result<()> { 61 | // build our application with a single route 62 | let app = Router::new() 63 | .route("/", get(|| async { "Hello, World!" })) 64 | .route( 65 | "/bid/", 66 | post({ 67 | let even_writer = even_writer.clone(); 68 | let persistence = persistence.clone(); 69 | 70 | |Json(bid_request): Json| async move { 71 | match handle_bid_request(persistence, even_writer, bid_request).await { 72 | Ok(()) => (StatusCode::OK, "".into()), 73 | Err(e) => handle_anyhow_error(e).await, 74 | } 75 | } 76 | }), 77 | ); 78 | 79 | // run it with hyper on localhost:3000 80 | axum::Server::try_bind(&"0.0.0.0:3000".parse()?)? 81 | .serve(app.into_make_service()) 82 | .await?; 83 | 84 | Ok(()) 85 | } 86 | 87 | async fn handle_anyhow_error(err: anyhow::Error) -> (StatusCode, String) { 88 | ( 89 | StatusCode::INTERNAL_SERVER_ERROR, 90 | format!("Something went wrong: {}", err), 91 | ) 92 | } 93 | 94 | impl Ui { 95 | pub fn new( 96 | persistence: SharedPersistence, 97 | even_writer: event_log::SharedWriter, 98 | ) -> Result { 99 | let runtime = Runtime::new()?; 100 | 101 | let (tx, rx) = oneshot::channel(); 102 | 103 | runtime.spawn(async move { 104 | tx.send( 105 | run_http_server(persistence, even_writer) 106 | .await 107 | .with_context(|| "Failed to run http server".to_string()), 108 | ) 109 | .expect("send to work"); 110 | }); 111 | 112 | Ok(Self { 113 | _runtime: runtime, 114 | server_rx: rx, 115 | }) 116 | } 117 | } 118 | 119 | impl LoopService for Ui { 120 | fn run_iteration<'a>(&mut self) -> Result<()> { 121 | // don't hog the cpu 122 | std::thread::sleep(std::time::Duration::from_millis(100)); 123 | 124 | match self.server_rx.try_recv() { 125 | Ok(res) => res, 126 | Err(oneshot::error::TryRecvError::Empty) => Ok(()), 127 | Err(oneshot::error::TryRecvError::Closed) => { 128 | Err(format_err!("ui server died without leaving a response?!")) 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/service.rs: -------------------------------------------------------------------------------- 1 | pub mod auction_house; 2 | pub mod bidding_engine; 3 | pub mod ui; 4 | 5 | pub use self::{auction_house::*, bidding_engine::*, ui::*}; 6 | use crate::{ 7 | event::Event, 8 | event_log::{self, WithOffset}, 9 | persistence::{Persistence, SharedPersistence, Transaction}, 10 | progress, 11 | }; 12 | use anyhow::{bail, format_err, Result}; 13 | use std::{ 14 | sync::{ 15 | atomic::{self, AtomicBool, Ordering}, 16 | Arc, 17 | }, 18 | thread, 19 | }; 20 | 21 | pub type ServiceId = String; 22 | pub type ServiceIdRef<'a> = &'a str; 23 | 24 | /// A service that handles events on the log 25 | pub trait LogFollowerService: Send + Sync { 26 | fn get_log_progress_id(&self) -> String; 27 | 28 | fn handle_event(&mut self, transaction: &mut dyn Transaction<'_>, event: Event) -> Result<()>; 29 | } 30 | 31 | /// A service that is a loop that does something 32 | pub trait LoopService: Send + Sync { 33 | fn run_iteration(&mut self) -> Result<()>; 34 | } 35 | 36 | /// Service execution control instance 37 | /// 38 | /// All services are basically a loop, and we would like to be able to 39 | /// gracefully terminate them, and handle and top-level error of any 40 | /// of them by gracefully stopping everything else. 41 | #[derive(Clone)] 42 | pub struct ServiceControl { 43 | stop_all: Arc, 44 | progress_store: progress::SharedProgressTracker, 45 | persistence: Arc, 46 | } 47 | 48 | impl ServiceControl { 49 | pub fn new( 50 | persistence: SharedPersistence, 51 | progress_store: progress::SharedProgressTracker, 52 | ) -> Self { 53 | Self { 54 | stop_all: Default::default(), 55 | progress_store, 56 | persistence, 57 | } 58 | } 59 | 60 | // Notify all spawned service instances to shutdown 61 | pub fn send_stop_to_all(&self) { 62 | self.stop_all.store(true, Ordering::SeqCst); 63 | } 64 | 65 | /// Spawn a service instance that implements a [`LogFollowerService`] 66 | /// to track log events from `event_reader`. 67 | pub fn spawn_log_follower( 68 | &self, 69 | mut service: impl LogFollowerService + 'static, 70 | event_reader: event_log::SharedReader, 71 | ) -> JoinHandle { 72 | self.spawn_event_loop( 73 | &service.get_log_progress_id(), 74 | event_reader, 75 | move |transaction, event_details| service.handle_event(transaction, event_details), 76 | ) 77 | } 78 | 79 | pub fn spawn_loop(&self, mut service: impl LoopService + 'static) -> JoinHandle { 80 | self.spawn_loop_raw(move || service.run_iteration()) 81 | } 82 | 83 | /// Start a new service as a loop, with a certain body 84 | /// 85 | /// This will take care of checking termination condition and 86 | /// handling any errors returned by `f` 87 | fn spawn_loop_raw(&self, mut f: F) -> JoinHandle 88 | where 89 | F: FnMut() -> Result<()> + Send + Sync + 'static, 90 | { 91 | let stop = Arc::new(AtomicBool::new(false)); 92 | 93 | JoinHandle::new( 94 | stop.clone(), 95 | thread::spawn({ 96 | let stop_all = self.stop_all.clone(); 97 | move || match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 98 | while !stop.load(atomic::Ordering::SeqCst) 99 | && !stop_all.load(atomic::Ordering::SeqCst) 100 | { 101 | if let Err(e) = f() { 102 | stop_all.store(true, atomic::Ordering::SeqCst); 103 | return Err(e); 104 | } 105 | } 106 | Ok(()) 107 | })) { 108 | Err(_e) => { 109 | stop_all.store(true, atomic::Ordering::SeqCst); 110 | bail!("service panicked"); 111 | } 112 | Ok(res) => res, 113 | } 114 | }), 115 | ) 116 | } 117 | 118 | fn spawn_event_loop( 119 | &self, 120 | service_id: ServiceIdRef, 121 | event_reader: event_log::SharedReader, 122 | mut f: F, 123 | ) -> JoinHandle 124 | where 125 | F: for<'a> FnMut(&mut dyn Transaction<'a>, Event) -> Result<()> + Send + Sync + 'static, 126 | { 127 | let service_id = service_id.to_owned(); 128 | 129 | let mut progress = { 130 | match (|| { 131 | let mut connection = self.persistence.get_connection()?; 132 | Ok( 133 | if let Some(offset) = self.progress_store.load(&mut *connection, &service_id)? { 134 | offset 135 | } else { 136 | event_reader.get_start_offset()? 137 | }, 138 | ) 139 | })() { 140 | // To avoid returning a `Result` directly from here, spawn a thread that will immediately terminate with an error, 141 | // just like the initial progress load was done from the spawned thread itself. 142 | Err(e) => { 143 | return JoinHandle::new( 144 | Arc::new(AtomicBool::new(false)), 145 | thread::spawn(move || Err(e)), 146 | ) 147 | } 148 | Ok(o) => o, 149 | } 150 | }; 151 | 152 | self.spawn_loop_raw({ 153 | let progress_store = self.progress_store.clone(); 154 | let persistence = self.persistence.clone(); 155 | move || { 156 | let mut connection = persistence.get_connection()?; 157 | 158 | let WithOffset { 159 | offset: new_offset, 160 | data: mut events, 161 | } = event_reader.read( 162 | &mut *connection, 163 | progress, 164 | 1, 165 | Some(std::time::Duration::from_secs(1)), 166 | )?; 167 | 168 | let mut transaction = connection.start_transaction()?; 169 | 170 | for event in events.drain(..) { 171 | f(&mut *transaction, event.details)?; 172 | 173 | progress = new_offset; 174 | progress_store.store_tr(&mut *transaction, &service_id, new_offset)?; 175 | } 176 | transaction.commit()?; 177 | Ok(()) 178 | } 179 | }) 180 | } 181 | } 182 | 183 | /// Simple thread join wrapper that joins the thread on drop 184 | /// 185 | /// TODO: Would it be better to have it set the `stop` flag toc terminate all threads 186 | /// on drop? 187 | pub struct JoinHandle { 188 | stop: Arc, 189 | thread: Option>>, 190 | } 191 | 192 | impl JoinHandle { 193 | fn new(stop: Arc, handle: thread::JoinHandle>) -> Self { 194 | JoinHandle { 195 | stop, 196 | thread: Some(handle), 197 | } 198 | } 199 | } 200 | 201 | impl JoinHandle { 202 | fn join_mut(&mut self) -> Result<()> { 203 | if let Some(h) = self.thread.take() { 204 | h.join().map_err(|e| format_err!("join failed: {:?}", e))? 205 | } else { 206 | Ok(()) 207 | } 208 | } 209 | 210 | #[allow(unused)] 211 | pub fn join(mut self) -> Result<()> { 212 | self.join_mut() 213 | } 214 | } 215 | 216 | impl Drop for JoinHandle { 217 | fn drop(&mut self) { 218 | self.stop.store(true, Ordering::SeqCst); 219 | self.join_mut().expect("not failed") 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/tests/bidding_engine.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | auction, 3 | auction::{Amount, BidDetails, Bidder, ItemBid, ItemIdRef}, 4 | event::{BiddingEngineEvent, Event, UiEvent}, 5 | event_log, 6 | persistence::{self, Connection, Persistence}, 7 | service, 8 | service::{bidding_engine::*, LogFollowerService}, 9 | }; 10 | use anyhow::Result; 11 | 12 | trait BiddingEngineTestExt { 13 | fn handle_max_bid_event( 14 | &mut self, 15 | conn: &mut dyn Connection, 16 | id: ItemIdRef, 17 | price: Amount, 18 | ) -> Result<()>; 19 | } 20 | 21 | impl BiddingEngineTestExt for BiddingEngine { 22 | fn handle_max_bid_event<'a>( 23 | &mut self, 24 | conn: &mut dyn Connection, 25 | id: ItemIdRef, 26 | price: Amount, 27 | ) -> Result<()> { 28 | self.handle_event( 29 | &mut *conn.start_transaction()?, 30 | Event::Ui(UiEvent::MaxBidSet(auction::ItemBid { 31 | item: id.to_owned(), 32 | price, 33 | })), 34 | ) 35 | } 36 | } 37 | 38 | #[test] 39 | fn sanity_check_sends_a_bid_when_asked_to_via_event_log() -> Result<()> { 40 | let persistence = persistence::InMemoryPersistence::new(); 41 | let mut conn = persistence.get_connection()?; 42 | 43 | let (event_writer, event_reader) = event_log::new_in_memory_shared()?; 44 | let bidding_state_store = service::bidding_engine::InMemoryBiddingStateStore::new_shared(); 45 | 46 | let mut bidding_engine = 47 | service::bidding_engine::BiddingEngine::new(bidding_state_store, event_writer); 48 | 49 | bidding_engine.handle_max_bid_event(&mut *conn, "foo", 100)?; 50 | 51 | let res = event_reader.read_one(&mut *conn, event_reader.get_start_offset()?)?; 52 | 53 | assert_eq!( 54 | res.data.clone().map(|e| e.details), 55 | Some(Event::BiddingEngine(BiddingEngineEvent::Bid(ItemBid { 56 | item: "foo".to_owned(), 57 | price: 0 58 | }))) 59 | ); 60 | 61 | let res = event_reader.read_one(&mut *conn, res.offset)?; 62 | assert_eq!(res.data.map(|e| e.details), None); 63 | 64 | // sending the same bid again makes no difference 65 | bidding_engine.handle_max_bid_event(&mut *conn, "foo", 100)?; 66 | 67 | let res = event_reader.read_one(&mut *conn, res.offset)?; 68 | assert_eq!(res.data.map(|e| e.details), None); 69 | Ok(()) 70 | } 71 | 72 | #[test] 73 | fn sends_an_initial_bid_when_max_bid_limit_set() -> Result<()> { 74 | assert_eq!( 75 | BiddingEngine::handle_max_bid_limit_event("foo", None, 100)?, 76 | ( 77 | Some(AuctionBiddingState { 78 | max_bid_limit: 100, 79 | last_bid_sent: Some(0), 80 | auction_state: AuctionState { 81 | higest_bid: None, 82 | closed: false 83 | }, 84 | }), 85 | vec![BiddingEngineEvent::Bid(ItemBid { 86 | item: "foo".to_string(), 87 | price: 0 88 | })] 89 | ) 90 | ); 91 | 92 | Ok(()) 93 | } 94 | 95 | #[test] 96 | fn sends_a_new_bid_when_max_bid_limit_raised() -> Result<()> { 97 | assert_eq!( 98 | BiddingEngine::handle_max_bid_limit_event( 99 | "foo", 100 | Some(AuctionBiddingState { 101 | max_bid_limit: 100, 102 | last_bid_sent: Some(0), 103 | auction_state: AuctionState { 104 | higest_bid: Some(BidDetails { 105 | bidder: Bidder::Other, 106 | increment: 1, 107 | price: 100 108 | }), 109 | closed: false 110 | }, 111 | }), 112 | 101 113 | )?, 114 | ( 115 | Some(AuctionBiddingState { 116 | max_bid_limit: 101, 117 | last_bid_sent: Some(101), 118 | auction_state: AuctionState { 119 | higest_bid: Some(BidDetails { 120 | bidder: Bidder::Other, 121 | increment: 1, 122 | price: 100 123 | }), 124 | closed: false 125 | }, 126 | }), 127 | vec![BiddingEngineEvent::Bid(ItemBid { 128 | item: "foo".to_string(), 129 | price: 101 130 | })] 131 | ) 132 | ); 133 | 134 | Ok(()) 135 | } 136 | 137 | #[test] 138 | fn doesnt_send_a_new_bid_when_max_bid_limit_raised_but_not_enough_to_outbid() -> Result<()> { 139 | assert_eq!( 140 | BiddingEngine::handle_max_bid_limit_event( 141 | "foo", 142 | Some(AuctionBiddingState { 143 | max_bid_limit: 100, 144 | last_bid_sent: Some(0), 145 | auction_state: AuctionState { 146 | higest_bid: Some(BidDetails { 147 | bidder: Bidder::Other, 148 | increment: 1, 149 | price: 101 150 | }), 151 | closed: false 152 | }, 153 | }), 154 | 101 155 | )?, 156 | ( 157 | Some(AuctionBiddingState { 158 | max_bid_limit: 101, 159 | last_bid_sent: Some(0), 160 | auction_state: AuctionState { 161 | higest_bid: Some(BidDetails { 162 | bidder: Bidder::Other, 163 | increment: 1, 164 | price: 101 165 | }), 166 | closed: false 167 | }, 168 | }), 169 | vec![] 170 | ) 171 | ); 172 | 173 | Ok(()) 174 | } 175 | 176 | #[test] 177 | fn doesnt_send_a_new_bid_when_max_bid_limit_raised_but_we_are_already_winning() -> Result<()> { 178 | assert_eq!( 179 | BiddingEngine::handle_max_bid_limit_event( 180 | "foo", 181 | Some(AuctionBiddingState { 182 | max_bid_limit: 100, 183 | last_bid_sent: Some(0), 184 | auction_state: AuctionState { 185 | higest_bid: Some(BidDetails { 186 | bidder: Bidder::Sniper, 187 | increment: 1, 188 | price: 0 189 | }), 190 | closed: false 191 | }, 192 | }), 193 | 101 194 | )?, 195 | ( 196 | Some(AuctionBiddingState { 197 | max_bid_limit: 101, 198 | last_bid_sent: Some(0), 199 | auction_state: AuctionState { 200 | higest_bid: Some(BidDetails { 201 | bidder: Bidder::Sniper, 202 | increment: 1, 203 | price: 0 204 | }), 205 | closed: false 206 | }, 207 | }), 208 | vec![] 209 | ) 210 | ); 211 | 212 | Ok(()) 213 | } 214 | 215 | #[test] 216 | fn doesnt_send_a_new_bid_when_max_bid_limit_raised_but_we_are_already_have_a_good_bid_sent( 217 | ) -> Result<()> { 218 | assert_eq!( 219 | BiddingEngine::handle_max_bid_limit_event( 220 | "foo", 221 | Some(AuctionBiddingState { 222 | max_bid_limit: 100, 223 | last_bid_sent: Some(10), 224 | auction_state: AuctionState { 225 | higest_bid: Some(BidDetails { 226 | bidder: Bidder::Other, 227 | increment: 1, 228 | price: 1 229 | }), 230 | closed: false 231 | }, 232 | }), 233 | 101 234 | )?, 235 | ( 236 | Some(AuctionBiddingState { 237 | max_bid_limit: 101, 238 | last_bid_sent: Some(10), 239 | auction_state: AuctionState { 240 | higest_bid: Some(BidDetails { 241 | bidder: Bidder::Other, 242 | increment: 1, 243 | price: 1 244 | }), 245 | closed: false 246 | }, 247 | }), 248 | vec![] 249 | ) 250 | ); 251 | 252 | Ok(()) 253 | } 254 | 255 | #[test] 256 | fn sends_a_new_bid_when_someone_outbids() -> Result<()> { 257 | assert_eq!( 258 | BiddingEngine::handle_auction_house_event( 259 | "foo", 260 | Some(AuctionBiddingState { 261 | max_bid_limit: 100, 262 | last_bid_sent: Some(10), 263 | auction_state: AuctionState { 264 | higest_bid: Some(BidDetails { 265 | bidder: Bidder::Sniper, 266 | increment: 1, 267 | price: 10 268 | }), 269 | closed: false 270 | }, 271 | }), 272 | crate::event::AuctionHouseItemEvent::Bid(BidDetails { 273 | bidder: Bidder::Other, 274 | price: 11, 275 | increment: 1 276 | }), 277 | )?, 278 | ( 279 | Some(AuctionBiddingState { 280 | max_bid_limit: 100, 281 | last_bid_sent: Some(12), 282 | auction_state: AuctionState { 283 | higest_bid: Some(BidDetails { 284 | bidder: Bidder::Other, 285 | increment: 1, 286 | price: 11 287 | }), 288 | closed: false 289 | }, 290 | }), 291 | vec![BiddingEngineEvent::Bid(ItemBid { 292 | item: "foo".to_string(), 293 | price: 12 294 | })] 295 | ) 296 | ); 297 | 298 | Ok(()) 299 | } 300 | -------------------------------------------------------------------------------- /src/service/bidding_engine.rs: -------------------------------------------------------------------------------- 1 | //! Bidding Engine 2 | //! 3 | //! The logic that based on events from the Ui and Auction House 4 | //! determines if new bids should be created and of what amount. 5 | use crate::{ 6 | auction::{Amount, BidDetails, Bidder, ItemBid, ItemId, ItemIdRef}, 7 | event::{AuctionHouseItemEvent, BiddingEngineAuctionError, BiddingEngineEvent, Event, UiEvent}, 8 | event_log, 9 | persistence::{Connection, InMemoryTransaction, Transaction}, 10 | service, 11 | }; 12 | use anyhow::Result; 13 | use std::{ 14 | collections::BTreeMap, 15 | sync::{Arc, Mutex}, 16 | }; 17 | use tracing::{debug, span, Level}; 18 | 19 | mod postgres; 20 | 21 | /// A store for the current state of each auction we participate in 22 | pub trait BiddingStateStore { 23 | fn load_tr( 24 | &self, 25 | conn: &mut dyn Transaction<'_>, 26 | item_id: ItemIdRef, 27 | ) -> Result>; 28 | 29 | fn store_tr( 30 | &self, 31 | conn: &mut dyn Transaction<'_>, 32 | item_id: ItemIdRef, 33 | state: AuctionBiddingState, 34 | ) -> Result<()>; 35 | 36 | fn load( 37 | &self, 38 | conn: &mut dyn Connection, 39 | item_id: ItemIdRef, 40 | ) -> Result> { 41 | self.load_tr(&mut *conn.start_transaction()?, item_id) 42 | } 43 | 44 | fn store( 45 | &self, 46 | conn: &mut dyn Connection, 47 | item_id: ItemIdRef, 48 | state: AuctionBiddingState, 49 | ) -> Result<()> { 50 | self.store_tr(&mut *conn.start_transaction()?, item_id, state) 51 | } 52 | } 53 | 54 | pub type SharedBiddingStateStore = Arc; 55 | 56 | pub struct InMemoryBiddingStateStore(Mutex>); 57 | 58 | impl InMemoryBiddingStateStore { 59 | pub fn new() -> Self { 60 | Self(Mutex::new(BTreeMap::default())) 61 | } 62 | 63 | pub fn new_shared() -> SharedBiddingStateStore { 64 | Arc::new(Self::new()) 65 | } 66 | } 67 | 68 | impl BiddingStateStore for InMemoryBiddingStateStore { 69 | fn load_tr<'a>( 70 | &self, 71 | conn: &mut dyn Transaction, 72 | item_id: ItemIdRef, 73 | ) -> Result> { 74 | conn.cast().as_mut::()?; 75 | Ok(self.0.lock().expect("lock").get(item_id).cloned()) 76 | } 77 | 78 | fn store_tr<'a>( 79 | &self, 80 | conn: &mut dyn Transaction, 81 | item_id: ItemIdRef, 82 | state: AuctionBiddingState, 83 | ) -> Result<()> { 84 | conn.cast().as_mut::()?; 85 | self.0 86 | .lock() 87 | .expect("lock") 88 | .insert(item_id.to_owned(), state); 89 | Ok(()) 90 | } 91 | } 92 | 93 | /// Bidding state from a perspective of the auction house 94 | /// 95 | /// Constructed from the events delivered from the (remote) Auction House. 96 | #[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] 97 | pub struct AuctionState { 98 | pub higest_bid: Option, 99 | pub closed: bool, 100 | } 101 | 102 | impl AuctionState { 103 | pub fn handle_auction_event(mut self, event: AuctionHouseItemEvent) -> Self { 104 | match event { 105 | AuctionHouseItemEvent::Bid(bid) => { 106 | if !self.closed 107 | && self 108 | .higest_bid 109 | .map(|highest| highest.is_outbidded_by(bid.price)) 110 | .unwrap_or(true) 111 | { 112 | self.higest_bid = Some(bid); 113 | } 114 | self 115 | } 116 | AuctionHouseItemEvent::Closed => { 117 | self.closed = true; 118 | self 119 | } 120 | } 121 | } 122 | 123 | /* 124 | fn event(self, event: Event) -> Result { 125 | use Event::*; 126 | Ok(match event { 127 | Bid(bid) => { 128 | self.ensure_valid_bid(bid)?; 129 | Self { 130 | higest_bid: Some(bid), 131 | ..self 132 | } 133 | } 134 | Closed => Self { 135 | closed: true, 136 | ..self 137 | }, 138 | }) 139 | } 140 | 141 | fn ensure_valid_bid(self, bid: BidDetails) -> Result<(), EventError> { 142 | use EventError::*; 143 | 144 | if self.closed { 145 | return Err(AlreadyClosed); 146 | } 147 | if let Some(highest_bid) = self.higest_bid { 148 | if !highest_bid.is_outbidded_by(bid.price) { 149 | return Err(TooLow); 150 | } 151 | } 152 | Ok(()) 153 | } 154 | */ 155 | 156 | fn get_next_valid_bid(self, max_price: Amount) -> Option { 157 | if self.closed { 158 | return None; 159 | } 160 | 161 | match self.higest_bid { 162 | // TODO: is 0 a valid bid? :) 163 | None => Some(0), 164 | 165 | // our bid is the higest already 166 | Some(BidDetails { 167 | bidder: Bidder::Sniper, 168 | .. 169 | }) => None, 170 | Some(highest_bid) => { 171 | let outbid_price = highest_bid.next_valid_bid(); 172 | if dbg!(outbid_price) <= dbg!(max_price) { 173 | Some(outbid_price) 174 | } else { 175 | None 176 | } 177 | } 178 | } 179 | } 180 | } 181 | 182 | #[derive(Copy, Clone, Default, PartialEq, Debug)] 183 | pub struct AuctionBiddingState { 184 | pub max_bid_limit: Amount, 185 | pub last_bid_sent: Option, 186 | pub auction_state: AuctionState, 187 | } 188 | 189 | impl AuctionBiddingState { 190 | pub fn is_bid_better_than_last_bid_sent(self, amount: Amount) -> bool { 191 | self.last_bid_sent.is_none() || self.last_bid_sent.unwrap_or(0) < amount 192 | } 193 | 194 | pub fn handle_auction_house_event(self, event: AuctionHouseItemEvent) -> Self { 195 | Self { 196 | auction_state: self.auction_state.handle_auction_event(event), 197 | ..self 198 | } 199 | } 200 | } 201 | 202 | pub const BIDDING_ENGINE_SERVICE_ID: &str = "bidding-engine"; 203 | 204 | pub struct BiddingEngine { 205 | bidding_state_store: SharedBiddingStateStore, 206 | event_writer: event_log::SharedWriter, 207 | } 208 | 209 | impl BiddingEngine { 210 | pub fn new( 211 | bidding_state_store: SharedBiddingStateStore, 212 | event_writer: event_log::SharedWriter, 213 | ) -> Self { 214 | Self { 215 | bidding_state_store, 216 | event_writer, 217 | } 218 | } 219 | 220 | fn handle_auction_item_event_with( 221 | &self, 222 | transaction: &mut dyn Transaction<'_>, 223 | item_id: ItemIdRef, 224 | data: T, 225 | f: impl FnOnce( 226 | ItemIdRef, 227 | Option, 228 | T, 229 | ) -> Result<(Option, Vec)>, 230 | ) -> Result<()> { 231 | let old_auction_state = self.bidding_state_store.load_tr(transaction, item_id)?; 232 | 233 | let (new_auction_state, events) = f(item_id, old_auction_state, data)?; 234 | 235 | if let Some(new_state) = new_auction_state { 236 | if Some(new_state) != old_auction_state { 237 | self.bidding_state_store 238 | .store_tr(transaction, item_id, new_state)?; 239 | } 240 | } 241 | 242 | debug!(?events, "write events"); 243 | self.event_writer.write_tr( 244 | transaction, 245 | &events 246 | .into_iter() 247 | .map(Event::BiddingEngine) 248 | .collect::>(), 249 | )?; 250 | 251 | Ok(()) 252 | } 253 | 254 | pub fn handle_auction_house_event( 255 | item_id: ItemIdRef, 256 | old_state: Option, 257 | event: AuctionHouseItemEvent, 258 | ) -> Result<(Option, Vec)> { 259 | if let Some(auction_state) = old_state { 260 | Self::handle_next_bid_decision_for_new_state( 261 | item_id, 262 | auction_state.handle_auction_house_event(event), 263 | ) 264 | } else { 265 | Ok(( 266 | None, 267 | vec![BiddingEngineEvent::AuctionError( 268 | BiddingEngineAuctionError::UnknownAuction(item_id.to_owned()), 269 | )], 270 | )) 271 | } 272 | } 273 | 274 | pub fn handle_max_bid_limit_event( 275 | item_id: ItemIdRef, 276 | old_state: Option, 277 | price: Amount, 278 | ) -> Result<(Option, Vec)> { 279 | let old_state = old_state.unwrap_or_default(); 280 | 281 | Self::handle_next_bid_decision_for_new_state( 282 | item_id, 283 | AuctionBiddingState { 284 | max_bid_limit: price, 285 | ..old_state 286 | }, 287 | ) 288 | } 289 | 290 | pub fn handle_next_bid_decision_for_new_state( 291 | item_id: ItemIdRef, 292 | mut new_state: AuctionBiddingState, 293 | ) -> Result<(Option, Vec)> { 294 | if let Some(our_new_bid) = new_state 295 | .auction_state 296 | .get_next_valid_bid(new_state.max_bid_limit) 297 | { 298 | if new_state.is_bid_better_than_last_bid_sent(our_new_bid) { 299 | new_state.last_bid_sent = Some(our_new_bid); 300 | 301 | Ok(( 302 | Some(new_state), 303 | vec![BiddingEngineEvent::Bid(ItemBid { 304 | item: item_id.to_owned(), 305 | price: our_new_bid, 306 | })], 307 | )) 308 | } else { 309 | Ok((Some(new_state), vec![])) 310 | } 311 | } else { 312 | Ok((Some(new_state), vec![])) 313 | } 314 | } 315 | } 316 | 317 | impl service::LogFollowerService for BiddingEngine { 318 | fn handle_event(&mut self, transaction: &mut dyn Transaction<'_>, event: Event) -> Result<()> { 319 | let span = span!(Level::DEBUG, "bidding engine - handle event"); 320 | let _guard = span.enter(); 321 | debug!(?event, "event"); 322 | match event { 323 | Event::AuctionHouse(event) => self.handle_auction_item_event_with( 324 | transaction, 325 | &event.item, 326 | event.event, 327 | Self::handle_auction_house_event, 328 | )?, 329 | Event::Ui(UiEvent::MaxBidSet(item_bid)) => self.handle_auction_item_event_with( 330 | transaction, 331 | &item_bid.item, 332 | item_bid.price, 333 | Self::handle_max_bid_limit_event, 334 | )?, 335 | _ => (), 336 | }; 337 | Ok(()) 338 | } 339 | 340 | fn get_log_progress_id(&self) -> String { 341 | BIDDING_ENGINE_SERVICE_ID.into() 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.71" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" 10 | 11 | [[package]] 12 | name = "async-condvar-fair" 13 | version = "1.0.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "d71f29de8959d91697251c72022950702f9980f8aadace37deae754c536f3c21" 16 | dependencies = [ 17 | "dlv-list", 18 | "parking_lot", 19 | "pin-project-lite", 20 | "tokio", 21 | ] 22 | 23 | [[package]] 24 | name = "async-trait" 25 | version = "0.1.68" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" 28 | dependencies = [ 29 | "proc-macro2", 30 | "quote", 31 | "syn", 32 | ] 33 | 34 | [[package]] 35 | name = "autocfg" 36 | version = "1.1.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 39 | 40 | [[package]] 41 | name = "axum" 42 | version = "0.6.18" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39" 45 | dependencies = [ 46 | "async-trait", 47 | "axum-core", 48 | "bitflags", 49 | "bytes", 50 | "futures-util", 51 | "http", 52 | "http-body", 53 | "hyper", 54 | "itoa", 55 | "matchit", 56 | "memchr", 57 | "mime", 58 | "percent-encoding", 59 | "pin-project-lite", 60 | "rustversion", 61 | "serde", 62 | "serde_json", 63 | "serde_path_to_error", 64 | "serde_urlencoded", 65 | "sync_wrapper", 66 | "tokio", 67 | "tower", 68 | "tower-layer", 69 | "tower-service", 70 | ] 71 | 72 | [[package]] 73 | name = "axum-core" 74 | version = "0.3.4" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" 77 | dependencies = [ 78 | "async-trait", 79 | "bytes", 80 | "futures-util", 81 | "http", 82 | "http-body", 83 | "mime", 84 | "rustversion", 85 | "tower-layer", 86 | "tower-service", 87 | ] 88 | 89 | [[package]] 90 | name = "base64" 91 | version = "0.21.2" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" 94 | 95 | [[package]] 96 | name = "bitflags" 97 | version = "1.3.2" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 100 | 101 | [[package]] 102 | name = "block-buffer" 103 | version = "0.10.4" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 106 | dependencies = [ 107 | "generic-array", 108 | ] 109 | 110 | [[package]] 111 | name = "byteorder" 112 | version = "1.4.3" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 115 | 116 | [[package]] 117 | name = "bytes" 118 | version = "1.4.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" 121 | 122 | [[package]] 123 | name = "cfg-if" 124 | version = "1.0.0" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 127 | 128 | [[package]] 129 | name = "cpufeatures" 130 | version = "0.2.7" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" 133 | dependencies = [ 134 | "libc", 135 | ] 136 | 137 | [[package]] 138 | name = "crypto-common" 139 | version = "0.1.6" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 142 | dependencies = [ 143 | "generic-array", 144 | "typenum", 145 | ] 146 | 147 | [[package]] 148 | name = "ctrlc" 149 | version = "3.4.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "2a011bbe2c35ce9c1f143b7af6f94f29a167beb4cd1d29e6740ce836f723120e" 152 | dependencies = [ 153 | "nix", 154 | "windows-sys 0.48.0", 155 | ] 156 | 157 | [[package]] 158 | name = "digest" 159 | version = "0.10.7" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 162 | dependencies = [ 163 | "block-buffer", 164 | "crypto-common", 165 | "subtle", 166 | ] 167 | 168 | [[package]] 169 | name = "dlv-list" 170 | version = "0.4.0" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "bd1e4a14ee49d368aa9378b0d570c639179489b6b1f89ea558bce6a15ca5b96a" 173 | 174 | [[package]] 175 | name = "dyno" 176 | version = "0.1.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "b5a51357b1e1c5b34b1ce5b1308dfa67cab1089e2e6f1903c83186f20c76c16a" 179 | 180 | [[package]] 181 | name = "fallible-iterator" 182 | version = "0.2.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" 185 | 186 | [[package]] 187 | name = "fnv" 188 | version = "1.0.7" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 191 | 192 | [[package]] 193 | name = "form_urlencoded" 194 | version = "1.1.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 197 | dependencies = [ 198 | "percent-encoding", 199 | ] 200 | 201 | [[package]] 202 | name = "futures" 203 | version = "0.3.28" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" 206 | dependencies = [ 207 | "futures-channel", 208 | "futures-core", 209 | "futures-executor", 210 | "futures-io", 211 | "futures-sink", 212 | "futures-task", 213 | "futures-util", 214 | ] 215 | 216 | [[package]] 217 | name = "futures-channel" 218 | version = "0.3.28" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" 221 | dependencies = [ 222 | "futures-core", 223 | "futures-sink", 224 | ] 225 | 226 | [[package]] 227 | name = "futures-core" 228 | version = "0.3.28" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" 231 | 232 | [[package]] 233 | name = "futures-executor" 234 | version = "0.3.28" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" 237 | dependencies = [ 238 | "futures-core", 239 | "futures-task", 240 | "futures-util", 241 | ] 242 | 243 | [[package]] 244 | name = "futures-io" 245 | version = "0.3.28" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" 248 | 249 | [[package]] 250 | name = "futures-macro" 251 | version = "0.3.28" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" 254 | dependencies = [ 255 | "proc-macro2", 256 | "quote", 257 | "syn", 258 | ] 259 | 260 | [[package]] 261 | name = "futures-sink" 262 | version = "0.3.28" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" 265 | 266 | [[package]] 267 | name = "futures-task" 268 | version = "0.3.28" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" 271 | 272 | [[package]] 273 | name = "futures-util" 274 | version = "0.3.28" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" 277 | dependencies = [ 278 | "futures-channel", 279 | "futures-core", 280 | "futures-io", 281 | "futures-macro", 282 | "futures-sink", 283 | "futures-task", 284 | "memchr", 285 | "pin-project-lite", 286 | "pin-utils", 287 | "slab", 288 | ] 289 | 290 | [[package]] 291 | name = "generic-array" 292 | version = "0.14.7" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 295 | dependencies = [ 296 | "typenum", 297 | "version_check", 298 | ] 299 | 300 | [[package]] 301 | name = "getrandom" 302 | version = "0.2.9" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" 305 | dependencies = [ 306 | "cfg-if", 307 | "libc", 308 | "wasi", 309 | ] 310 | 311 | [[package]] 312 | name = "hermit-abi" 313 | version = "0.2.6" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 316 | dependencies = [ 317 | "libc", 318 | ] 319 | 320 | [[package]] 321 | name = "hmac" 322 | version = "0.12.1" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 325 | dependencies = [ 326 | "digest", 327 | ] 328 | 329 | [[package]] 330 | name = "http" 331 | version = "0.2.9" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" 334 | dependencies = [ 335 | "bytes", 336 | "fnv", 337 | "itoa", 338 | ] 339 | 340 | [[package]] 341 | name = "http-body" 342 | version = "0.4.5" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 345 | dependencies = [ 346 | "bytes", 347 | "http", 348 | "pin-project-lite", 349 | ] 350 | 351 | [[package]] 352 | name = "httparse" 353 | version = "1.8.0" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 356 | 357 | [[package]] 358 | name = "httpdate" 359 | version = "1.0.2" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 362 | 363 | [[package]] 364 | name = "hyper" 365 | version = "0.14.26" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" 368 | dependencies = [ 369 | "bytes", 370 | "futures-channel", 371 | "futures-core", 372 | "futures-util", 373 | "http", 374 | "http-body", 375 | "httparse", 376 | "httpdate", 377 | "itoa", 378 | "pin-project-lite", 379 | "socket2 0.4.9", 380 | "tokio", 381 | "tower-service", 382 | "tracing", 383 | "want", 384 | ] 385 | 386 | [[package]] 387 | name = "itoa" 388 | version = "1.0.6" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 391 | 392 | [[package]] 393 | name = "lazy_static" 394 | version = "1.4.0" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 397 | 398 | [[package]] 399 | name = "libc" 400 | version = "0.2.144" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" 403 | 404 | [[package]] 405 | name = "lock_api" 406 | version = "0.4.9" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 409 | dependencies = [ 410 | "autocfg", 411 | "scopeguard", 412 | ] 413 | 414 | [[package]] 415 | name = "log" 416 | version = "0.4.18" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" 419 | 420 | [[package]] 421 | name = "matchit" 422 | version = "0.7.0" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" 425 | 426 | [[package]] 427 | name = "md-5" 428 | version = "0.10.5" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" 431 | dependencies = [ 432 | "digest", 433 | ] 434 | 435 | [[package]] 436 | name = "memchr" 437 | version = "2.5.0" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 440 | 441 | [[package]] 442 | name = "mime" 443 | version = "0.3.17" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 446 | 447 | [[package]] 448 | name = "mio" 449 | version = "0.8.8" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" 452 | dependencies = [ 453 | "libc", 454 | "wasi", 455 | "windows-sys 0.48.0", 456 | ] 457 | 458 | [[package]] 459 | name = "nix" 460 | version = "0.26.2" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" 463 | dependencies = [ 464 | "bitflags", 465 | "cfg-if", 466 | "libc", 467 | "static_assertions", 468 | ] 469 | 470 | [[package]] 471 | name = "nu-ansi-term" 472 | version = "0.46.0" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 475 | dependencies = [ 476 | "overload", 477 | "winapi", 478 | ] 479 | 480 | [[package]] 481 | name = "num_cpus" 482 | version = "1.15.0" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 485 | dependencies = [ 486 | "hermit-abi", 487 | "libc", 488 | ] 489 | 490 | [[package]] 491 | name = "once_cell" 492 | version = "1.17.2" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" 495 | 496 | [[package]] 497 | name = "overload" 498 | version = "0.1.1" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 501 | 502 | [[package]] 503 | name = "parking_lot" 504 | version = "0.12.1" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 507 | dependencies = [ 508 | "lock_api", 509 | "parking_lot_core", 510 | ] 511 | 512 | [[package]] 513 | name = "parking_lot_core" 514 | version = "0.9.7" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" 517 | dependencies = [ 518 | "cfg-if", 519 | "libc", 520 | "redox_syscall", 521 | "smallvec", 522 | "windows-sys 0.45.0", 523 | ] 524 | 525 | [[package]] 526 | name = "percent-encoding" 527 | version = "2.2.0" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 530 | 531 | [[package]] 532 | name = "phf" 533 | version = "0.11.1" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" 536 | dependencies = [ 537 | "phf_shared", 538 | ] 539 | 540 | [[package]] 541 | name = "phf_shared" 542 | version = "0.11.1" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" 545 | dependencies = [ 546 | "siphasher", 547 | ] 548 | 549 | [[package]] 550 | name = "pin-project" 551 | version = "1.1.0" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" 554 | dependencies = [ 555 | "pin-project-internal", 556 | ] 557 | 558 | [[package]] 559 | name = "pin-project-internal" 560 | version = "1.1.0" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" 563 | dependencies = [ 564 | "proc-macro2", 565 | "quote", 566 | "syn", 567 | ] 568 | 569 | [[package]] 570 | name = "pin-project-lite" 571 | version = "0.2.9" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 574 | 575 | [[package]] 576 | name = "pin-utils" 577 | version = "0.1.0" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 580 | 581 | [[package]] 582 | name = "postgres" 583 | version = "0.19.5" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "0bed5017bc2ff49649c0075d0d7a9d676933c1292480c1d137776fb205b5cd18" 586 | dependencies = [ 587 | "bytes", 588 | "fallible-iterator", 589 | "futures-util", 590 | "log", 591 | "tokio", 592 | "tokio-postgres", 593 | ] 594 | 595 | [[package]] 596 | name = "postgres-protocol" 597 | version = "0.6.5" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "78b7fa9f396f51dffd61546fd8573ee20592287996568e6175ceb0f8699ad75d" 600 | dependencies = [ 601 | "base64", 602 | "byteorder", 603 | "bytes", 604 | "fallible-iterator", 605 | "hmac", 606 | "md-5", 607 | "memchr", 608 | "rand", 609 | "sha2", 610 | "stringprep", 611 | ] 612 | 613 | [[package]] 614 | name = "postgres-types" 615 | version = "0.2.5" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "f028f05971fe20f512bcc679e2c10227e57809a3af86a7606304435bc8896cd6" 618 | dependencies = [ 619 | "bytes", 620 | "fallible-iterator", 621 | "postgres-protocol", 622 | ] 623 | 624 | [[package]] 625 | name = "ppv-lite86" 626 | version = "0.2.17" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 629 | 630 | [[package]] 631 | name = "proc-macro2" 632 | version = "1.0.59" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" 635 | dependencies = [ 636 | "unicode-ident", 637 | ] 638 | 639 | [[package]] 640 | name = "quote" 641 | version = "1.0.28" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" 644 | dependencies = [ 645 | "proc-macro2", 646 | ] 647 | 648 | [[package]] 649 | name = "r2d2" 650 | version = "0.8.10" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" 653 | dependencies = [ 654 | "log", 655 | "parking_lot", 656 | "scheduled-thread-pool", 657 | ] 658 | 659 | [[package]] 660 | name = "r2d2_postgres" 661 | version = "0.18.1" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "7029c56be658cb54f321e0bee597810ee16796b735fa2559d7056bf06b12230b" 664 | dependencies = [ 665 | "postgres", 666 | "r2d2", 667 | ] 668 | 669 | [[package]] 670 | name = "rand" 671 | version = "0.8.5" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 674 | dependencies = [ 675 | "libc", 676 | "rand_chacha", 677 | "rand_core", 678 | ] 679 | 680 | [[package]] 681 | name = "rand_chacha" 682 | version = "0.3.1" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 685 | dependencies = [ 686 | "ppv-lite86", 687 | "rand_core", 688 | ] 689 | 690 | [[package]] 691 | name = "rand_core" 692 | version = "0.6.4" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 695 | dependencies = [ 696 | "getrandom", 697 | ] 698 | 699 | [[package]] 700 | name = "redox_syscall" 701 | version = "0.2.16" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 704 | dependencies = [ 705 | "bitflags", 706 | ] 707 | 708 | [[package]] 709 | name = "rustversion" 710 | version = "1.0.12" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" 713 | 714 | [[package]] 715 | name = "ryu" 716 | version = "1.0.13" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 719 | 720 | [[package]] 721 | name = "scheduled-thread-pool" 722 | version = "0.2.7" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" 725 | dependencies = [ 726 | "parking_lot", 727 | ] 728 | 729 | [[package]] 730 | name = "scopeguard" 731 | version = "1.1.0" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 734 | 735 | [[package]] 736 | name = "serde" 737 | version = "1.0.163" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" 740 | dependencies = [ 741 | "serde_derive", 742 | ] 743 | 744 | [[package]] 745 | name = "serde_derive" 746 | version = "1.0.163" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" 749 | dependencies = [ 750 | "proc-macro2", 751 | "quote", 752 | "syn", 753 | ] 754 | 755 | [[package]] 756 | name = "serde_json" 757 | version = "1.0.96" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" 760 | dependencies = [ 761 | "itoa", 762 | "ryu", 763 | "serde", 764 | ] 765 | 766 | [[package]] 767 | name = "serde_path_to_error" 768 | version = "0.1.11" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "f7f05c1d5476066defcdfacce1f52fc3cae3af1d3089727100c02ae92e5abbe0" 771 | dependencies = [ 772 | "serde", 773 | ] 774 | 775 | [[package]] 776 | name = "serde_urlencoded" 777 | version = "0.7.1" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 780 | dependencies = [ 781 | "form_urlencoded", 782 | "itoa", 783 | "ryu", 784 | "serde", 785 | ] 786 | 787 | [[package]] 788 | name = "sha2" 789 | version = "0.10.6" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" 792 | dependencies = [ 793 | "cfg-if", 794 | "cpufeatures", 795 | "digest", 796 | ] 797 | 798 | [[package]] 799 | name = "sharded-slab" 800 | version = "0.1.4" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 803 | dependencies = [ 804 | "lazy_static", 805 | ] 806 | 807 | [[package]] 808 | name = "siphasher" 809 | version = "0.3.10" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" 812 | 813 | [[package]] 814 | name = "slab" 815 | version = "0.4.8" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" 818 | dependencies = [ 819 | "autocfg", 820 | ] 821 | 822 | [[package]] 823 | name = "smallvec" 824 | version = "1.10.0" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 827 | 828 | [[package]] 829 | name = "sniper" 830 | version = "0.1.0" 831 | dependencies = [ 832 | "anyhow", 833 | "async-condvar-fair", 834 | "async-trait", 835 | "axum", 836 | "ctrlc", 837 | "dyno", 838 | "futures", 839 | "parking_lot", 840 | "postgres", 841 | "r2d2", 842 | "r2d2_postgres", 843 | "serde", 844 | "thiserror", 845 | "tokio", 846 | "tracing", 847 | "tracing-subscriber", 848 | ] 849 | 850 | [[package]] 851 | name = "socket2" 852 | version = "0.4.9" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" 855 | dependencies = [ 856 | "libc", 857 | "winapi", 858 | ] 859 | 860 | [[package]] 861 | name = "socket2" 862 | version = "0.5.3" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" 865 | dependencies = [ 866 | "libc", 867 | "windows-sys 0.48.0", 868 | ] 869 | 870 | [[package]] 871 | name = "static_assertions" 872 | version = "1.1.0" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 875 | 876 | [[package]] 877 | name = "stringprep" 878 | version = "0.1.2" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" 881 | dependencies = [ 882 | "unicode-bidi", 883 | "unicode-normalization", 884 | ] 885 | 886 | [[package]] 887 | name = "subtle" 888 | version = "2.5.0" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 891 | 892 | [[package]] 893 | name = "syn" 894 | version = "2.0.18" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" 897 | dependencies = [ 898 | "proc-macro2", 899 | "quote", 900 | "unicode-ident", 901 | ] 902 | 903 | [[package]] 904 | name = "sync_wrapper" 905 | version = "0.1.2" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 908 | 909 | [[package]] 910 | name = "thiserror" 911 | version = "1.0.40" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" 914 | dependencies = [ 915 | "thiserror-impl", 916 | ] 917 | 918 | [[package]] 919 | name = "thiserror-impl" 920 | version = "1.0.40" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" 923 | dependencies = [ 924 | "proc-macro2", 925 | "quote", 926 | "syn", 927 | ] 928 | 929 | [[package]] 930 | name = "thread_local" 931 | version = "1.1.7" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 934 | dependencies = [ 935 | "cfg-if", 936 | "once_cell", 937 | ] 938 | 939 | [[package]] 940 | name = "tinyvec" 941 | version = "1.6.0" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 944 | dependencies = [ 945 | "tinyvec_macros", 946 | ] 947 | 948 | [[package]] 949 | name = "tinyvec_macros" 950 | version = "0.1.1" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 953 | 954 | [[package]] 955 | name = "tokio" 956 | version = "1.28.2" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" 959 | dependencies = [ 960 | "autocfg", 961 | "bytes", 962 | "libc", 963 | "mio", 964 | "num_cpus", 965 | "pin-project-lite", 966 | "socket2 0.4.9", 967 | "windows-sys 0.48.0", 968 | ] 969 | 970 | [[package]] 971 | name = "tokio-postgres" 972 | version = "0.7.8" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "6e89f6234aa8fd43779746012fcf53603cdb91fdd8399aa0de868c2d56b6dde1" 975 | dependencies = [ 976 | "async-trait", 977 | "byteorder", 978 | "bytes", 979 | "fallible-iterator", 980 | "futures-channel", 981 | "futures-util", 982 | "log", 983 | "parking_lot", 984 | "percent-encoding", 985 | "phf", 986 | "pin-project-lite", 987 | "postgres-protocol", 988 | "postgres-types", 989 | "socket2 0.5.3", 990 | "tokio", 991 | "tokio-util", 992 | ] 993 | 994 | [[package]] 995 | name = "tokio-util" 996 | version = "0.7.8" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" 999 | dependencies = [ 1000 | "bytes", 1001 | "futures-core", 1002 | "futures-sink", 1003 | "pin-project-lite", 1004 | "tokio", 1005 | "tracing", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "tower" 1010 | version = "0.4.13" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 1013 | dependencies = [ 1014 | "futures-core", 1015 | "futures-util", 1016 | "pin-project", 1017 | "pin-project-lite", 1018 | "tokio", 1019 | "tower-layer", 1020 | "tower-service", 1021 | "tracing", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "tower-layer" 1026 | version = "0.3.2" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" 1029 | 1030 | [[package]] 1031 | name = "tower-service" 1032 | version = "0.3.2" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1035 | 1036 | [[package]] 1037 | name = "tracing" 1038 | version = "0.1.37" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 1041 | dependencies = [ 1042 | "cfg-if", 1043 | "log", 1044 | "pin-project-lite", 1045 | "tracing-attributes", 1046 | "tracing-core", 1047 | ] 1048 | 1049 | [[package]] 1050 | name = "tracing-attributes" 1051 | version = "0.1.24" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" 1054 | dependencies = [ 1055 | "proc-macro2", 1056 | "quote", 1057 | "syn", 1058 | ] 1059 | 1060 | [[package]] 1061 | name = "tracing-core" 1062 | version = "0.1.31" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" 1065 | dependencies = [ 1066 | "once_cell", 1067 | "valuable", 1068 | ] 1069 | 1070 | [[package]] 1071 | name = "tracing-log" 1072 | version = "0.1.3" 1073 | source = "registry+https://github.com/rust-lang/crates.io-index" 1074 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" 1075 | dependencies = [ 1076 | "lazy_static", 1077 | "log", 1078 | "tracing-core", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "tracing-subscriber" 1083 | version = "0.3.17" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" 1086 | dependencies = [ 1087 | "nu-ansi-term", 1088 | "sharded-slab", 1089 | "smallvec", 1090 | "thread_local", 1091 | "tracing-core", 1092 | "tracing-log", 1093 | ] 1094 | 1095 | [[package]] 1096 | name = "try-lock" 1097 | version = "0.2.4" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" 1100 | 1101 | [[package]] 1102 | name = "typenum" 1103 | version = "1.16.0" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" 1106 | 1107 | [[package]] 1108 | name = "unicode-bidi" 1109 | version = "0.3.13" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 1112 | 1113 | [[package]] 1114 | name = "unicode-ident" 1115 | version = "1.0.9" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" 1118 | 1119 | [[package]] 1120 | name = "unicode-normalization" 1121 | version = "0.1.22" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1124 | dependencies = [ 1125 | "tinyvec", 1126 | ] 1127 | 1128 | [[package]] 1129 | name = "valuable" 1130 | version = "0.1.0" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1133 | 1134 | [[package]] 1135 | name = "version_check" 1136 | version = "0.9.4" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1139 | 1140 | [[package]] 1141 | name = "want" 1142 | version = "0.3.0" 1143 | source = "registry+https://github.com/rust-lang/crates.io-index" 1144 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1145 | dependencies = [ 1146 | "log", 1147 | "try-lock", 1148 | ] 1149 | 1150 | [[package]] 1151 | name = "wasi" 1152 | version = "0.11.0+wasi-snapshot-preview1" 1153 | source = "registry+https://github.com/rust-lang/crates.io-index" 1154 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1155 | 1156 | [[package]] 1157 | name = "winapi" 1158 | version = "0.3.9" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1161 | dependencies = [ 1162 | "winapi-i686-pc-windows-gnu", 1163 | "winapi-x86_64-pc-windows-gnu", 1164 | ] 1165 | 1166 | [[package]] 1167 | name = "winapi-i686-pc-windows-gnu" 1168 | version = "0.4.0" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1171 | 1172 | [[package]] 1173 | name = "winapi-x86_64-pc-windows-gnu" 1174 | version = "0.4.0" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1177 | 1178 | [[package]] 1179 | name = "windows-sys" 1180 | version = "0.45.0" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1183 | dependencies = [ 1184 | "windows-targets 0.42.2", 1185 | ] 1186 | 1187 | [[package]] 1188 | name = "windows-sys" 1189 | version = "0.48.0" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1192 | dependencies = [ 1193 | "windows-targets 0.48.0", 1194 | ] 1195 | 1196 | [[package]] 1197 | name = "windows-targets" 1198 | version = "0.42.2" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 1201 | dependencies = [ 1202 | "windows_aarch64_gnullvm 0.42.2", 1203 | "windows_aarch64_msvc 0.42.2", 1204 | "windows_i686_gnu 0.42.2", 1205 | "windows_i686_msvc 0.42.2", 1206 | "windows_x86_64_gnu 0.42.2", 1207 | "windows_x86_64_gnullvm 0.42.2", 1208 | "windows_x86_64_msvc 0.42.2", 1209 | ] 1210 | 1211 | [[package]] 1212 | name = "windows-targets" 1213 | version = "0.48.0" 1214 | source = "registry+https://github.com/rust-lang/crates.io-index" 1215 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 1216 | dependencies = [ 1217 | "windows_aarch64_gnullvm 0.48.0", 1218 | "windows_aarch64_msvc 0.48.0", 1219 | "windows_i686_gnu 0.48.0", 1220 | "windows_i686_msvc 0.48.0", 1221 | "windows_x86_64_gnu 0.48.0", 1222 | "windows_x86_64_gnullvm 0.48.0", 1223 | "windows_x86_64_msvc 0.48.0", 1224 | ] 1225 | 1226 | [[package]] 1227 | name = "windows_aarch64_gnullvm" 1228 | version = "0.42.2" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 1231 | 1232 | [[package]] 1233 | name = "windows_aarch64_gnullvm" 1234 | version = "0.48.0" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 1237 | 1238 | [[package]] 1239 | name = "windows_aarch64_msvc" 1240 | version = "0.42.2" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 1243 | 1244 | [[package]] 1245 | name = "windows_aarch64_msvc" 1246 | version = "0.48.0" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 1249 | 1250 | [[package]] 1251 | name = "windows_i686_gnu" 1252 | version = "0.42.2" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 1255 | 1256 | [[package]] 1257 | name = "windows_i686_gnu" 1258 | version = "0.48.0" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 1261 | 1262 | [[package]] 1263 | name = "windows_i686_msvc" 1264 | version = "0.42.2" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 1267 | 1268 | [[package]] 1269 | name = "windows_i686_msvc" 1270 | version = "0.48.0" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 1273 | 1274 | [[package]] 1275 | name = "windows_x86_64_gnu" 1276 | version = "0.42.2" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1279 | 1280 | [[package]] 1281 | name = "windows_x86_64_gnu" 1282 | version = "0.48.0" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 1285 | 1286 | [[package]] 1287 | name = "windows_x86_64_gnullvm" 1288 | version = "0.42.2" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1291 | 1292 | [[package]] 1293 | name = "windows_x86_64_gnullvm" 1294 | version = "0.48.0" 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" 1296 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 1297 | 1298 | [[package]] 1299 | name = "windows_x86_64_msvc" 1300 | version = "0.42.2" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1303 | 1304 | [[package]] 1305 | name = "windows_x86_64_msvc" 1306 | version = "0.48.0" 1307 | source = "registry+https://github.com/rust-lang/crates.io-index" 1308 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 1309 | --------------------------------------------------------------------------------