Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 46 additions & 5 deletions crates/lyracore-shared/src/auction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,38 @@ pub fn minimum_next_bid(start_bid: u32, current_bid: u32) -> Option<u32> {
}
}

/// Stable reducer-boundary result tags for listing gameplay refusals.
pub mod result {
pub const DATABASE: &str = "AUCTION_DATABASE";
pub const ITEM_NOT_FOUND: &str = "AUCTION_ITEM_NOT_FOUND";
pub const NOT_ENOUGH_MONEY: &str = "AUCTION_NOT_ENOUGH_MONEY";
/// Why the Module refused an auction Durable Request. The tag is the whole reducer error text,
/// so neither tier matches on human prose.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AuctionRefusal {
ItemNotFound,
NotEnoughMoney,
InvalidTerms,
Database,
}

impl AuctionRefusal {
pub const ALL: [Self; 4] = [
Self::ItemNotFound,
Self::NotEnoughMoney,
Self::InvalidTerms,
Self::Database,
];

pub fn as_tag(self) -> &'static str {
match self {
Self::ItemNotFound => "auction:item_not_found",
Self::NotEnoughMoney => "auction:not_enough_money",
Self::InvalidTerms => "auction:invalid_terms",
Self::Database => "auction:database",
}
}

pub fn parse_tag(tag: &str) -> Option<Self> {
Self::ALL
.into_iter()
.find(|refusal| refusal.as_tag() == tag)
}
}

/// Stable terminal outcome codes shared by bid Hold and decision rows.
Expand All @@ -43,6 +70,20 @@ pub mod bid_outcome {

#[cfg(test)]
mod tests {
use super::AuctionRefusal;

#[test]
fn every_refusal_tag_round_trips() {
for refusal in AuctionRefusal::ALL {
assert_eq!(AuctionRefusal::parse_tag(refusal.as_tag()), Some(refusal));
}
assert_eq!(AuctionRefusal::parse_tag("auction:"), None);
assert_eq!(
AuctionRefusal::parse_tag("gw_auction_hold_bid reducer timed out after 10s"),
None
);
}

#[test]
fn bid_increment_is_five_percent_rounded_up() {
assert_eq!(super::bid_increment(0), 0);
Expand Down
109 changes: 44 additions & 65 deletions gateway/src/stdb/reducers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
use super::bindings::*;
use super::connection::{call_reducer, recv_reducer_on, reducer_refusal_reason, Coordinator};
use super::views::entity_view;
use lyracore_shared::auction::AuctionRefusal;

static NEXT_TAXI_REQUEST_ID: OnceLock<AtomicU64> = OnceLock::new();

Expand Down Expand Up @@ -276,8 +277,8 @@ impl Coordinator {

match result {
Ok(auction_id) => Ok(CreateAuctionOutcome::Created { auction_id }),
Err(error) => match map_auction_refusal(&error) {
Some(outcome) => Ok(outcome),
Err(error) => match auction_refusal(&error) {
Some(refusal) => Ok(refusal.into()),
None => Err(error),
},
}
Expand Down Expand Up @@ -533,8 +534,8 @@ impl Coordinator {
};
match result {
Ok(hold) => bid_outcome(&hold),
Err(error) => match map_bid_refusal(&error) {
Some(outcome) => Ok(outcome),
Err(error) => match auction_refusal(&error) {
Some(refusal) => Ok(refusal.into()),
None => Err(error),
},
}
Expand Down Expand Up @@ -621,7 +622,9 @@ impl Coordinator {
decision: &AuctionBidDecision,
) -> Result<()> {
if !bid_payload_matches(hold, decision) {
return Err(anyhow!("auction bid decision payload does not match its Hold"));
return Err(anyhow!(
"auction bid decision payload does not match its Hold"
));
}
call_reducer!(
self.0.call_pipe().conn.reducers,
Expand Down Expand Up @@ -713,9 +716,7 @@ impl Coordinator {
.game_auction_bid_hold()
.operation_id()
.find(&operation_id)
.filter(|hold| {
hold.outcome != lyracore_shared::auction::bid_outcome::PENDING
})
.filter(|hold| hold.outcome != lyracore_shared::auction::bid_outcome::PENDING)
})
}

Expand Down Expand Up @@ -3090,18 +3091,10 @@ fn next_auction_operation_id() -> Result<u64> {
}
}

fn map_auction_refusal(error: &anyhow::Error) -> Option<crate::world::CreateAuctionOutcome> {
use crate::world::CreateAuctionOutcome;
let text = format!("{error:#}");
if text.contains(lyracore_shared::auction::result::ITEM_NOT_FOUND) {
Some(CreateAuctionOutcome::ItemNotFound)
} else if text.contains(lyracore_shared::auction::result::NOT_ENOUGH_MONEY) {
Some(CreateAuctionOutcome::NotEnoughMoney)
} else if text.contains(lyracore_shared::auction::result::DATABASE) {
Some(CreateAuctionOutcome::Database)
} else {
None
}
/// The Module's typed auction Refusal. Only a reducer the Module rejected carries a tag; a timeout,
/// transport, or SDK failure stays an error with an unknown outcome.
fn auction_refusal(error: &anyhow::Error) -> Option<AuctionRefusal> {
reducer_refusal_reason(error).and_then(AuctionRefusal::parse_tag)
}

fn bid_payload_matches(hold: &AuctionBidHold, decision: &AuctionBidDecision) -> bool {
Expand Down Expand Up @@ -3146,63 +3139,49 @@ fn bid_outcome(hold: &AuctionBidHold) -> Result<crate::world::PlaceBidOutcome> {
bid_outcome::BID_INCREMENT => PlaceBidOutcome::BidIncrement,
bid_outcome::BID_OWN => PlaceBidOutcome::BidOwn,
bid_outcome::DATABASE => PlaceBidOutcome::Database,
outcome => return Err(anyhow!("auction bid Hold has non-terminal outcome {outcome}")),
outcome => {
return Err(anyhow!(
"auction bid Hold has non-terminal outcome {outcome}"
))
}
})
}

fn map_bid_refusal(error: &anyhow::Error) -> Option<crate::world::PlaceBidOutcome> {
use crate::world::PlaceBidOutcome;
let text = format!("{error:#}");
if text.contains(lyracore_shared::auction::result::NOT_ENOUGH_MONEY) {
Some(PlaceBidOutcome::NotEnoughMoney)
} else if text.contains(lyracore_shared::auction::result::DATABASE) {
Some(PlaceBidOutcome::Database)
} else {
None
}
}

#[cfg(test)]
mod auction_reducer_tests {
use super::*;
use crate::world::CreateAuctionOutcome;
use crate::stdb::connection::ReducerCallError;

#[test]
fn reducer_tags_map_to_typed_listing_outcomes() {
let cases = [
(
lyracore_shared::auction::result::ITEM_NOT_FOUND,
CreateAuctionOutcome::ItemNotFound,
),
(
lyracore_shared::auction::result::NOT_ENOUGH_MONEY,
CreateAuctionOutcome::NotEnoughMoney,
),
(
lyracore_shared::auction::result::DATABASE,
CreateAuctionOutcome::Database,
),
];
for (tag, expected) in cases {
assert_eq!(map_auction_refusal(&anyhow!("[{tag}] refused")), Some(expected));
fn only_a_rejected_reducer_carries_a_typed_refusal() {
for refusal in AuctionRefusal::ALL {
let error = anyhow::Error::from(ReducerCallError::Rejected {
operation: "gw_auction_hold_listing".to_string(),
reason: refusal.as_tag().to_string(),
})
.context("listing phase 1");
assert_eq!(auction_refusal(&error), Some(refusal));
}
assert_eq!(map_auction_refusal(&anyhow!("transport disconnected")), None);

assert_eq!(
map_bid_refusal(&anyhow!(
"[{}] refused",
lyracore_shared::auction::result::NOT_ENOUGH_MONEY
let not_refusals = [
anyhow::Error::from(ReducerCallError::fatal(
"gw_auction_hold_listing reducer timed out after 10s".to_string(),
)),
Some(crate::world::PlaceBidOutcome::NotEnoughMoney)
);
assert_eq!(
map_bid_refusal(&anyhow!(
"[{}] refused",
lyracore_shared::auction::result::DATABASE
anyhow::Error::from(ReducerCallError::fatal(
"gw_auction_hold_bid reducer failed: transport disconnected".to_string(),
)),
Some(crate::world::PlaceBidOutcome::Database)
);
assert_eq!(map_bid_refusal(&anyhow!("transport disconnected")), None);
anyhow::Error::from(ReducerCallError::Rejected {
operation: "gw_auction_hold_bid".to_string(),
reason: "operator only".to_string(),
}),
anyhow!(
"wrapped text that mentions {}",
AuctionRefusal::Database.as_tag()
),
];
for error in not_refusals {
assert_eq!(auction_refusal(&error), None, "{error:#}");
}
}

#[test]
Expand Down
Loading
Loading