feat(fifa17): wire async economy handlers into the host via a runtime bridge (unrouted)
Bridges the synchronous thread-per-connection host to the async
transfer-market/pile handlers WITHOUT flipping the classifier. classify()
is untouched; production still proxies every economy route to Python. The
new dispatch is exercised only by the integration harness via
Server::try_handle_economy — handler wiring, not authority cutover.
async_bridge.rs: AsyncBridge owns ONE process-lifetime multi-threaded Tokio
runtime, shared by every connection via Arc. block_on() runs a future from
the sync dispatch thread; if invoked from within an ambient runtime it
offloads onto its own runtime + a std channel instead of panicking
("cannot start a runtime from within a runtime"). 4 unit tests incl. the
nested-runtime-safety case and concurrent multi-thread drivers.
lib.rs: EconomyRoute + classify_economy (mirrors the Python route table:
credits, purchasegroup, store/transaction, purchased, item DELETE/PUT,
ut/delete match/item/trade, auctionhouse/transfermarket, tradePile, trade).
EconomyServices (Core econ transport + durable MarketStore/PileStore + the
bridge + the pack-content pool), attached via Server::with_economy (kept out
of `new`/`from_config` so existing tests build a DB-less Server; production
from_config attachment is the barrier step). Server::try_handle_economy
dispatches: sync handlers (credits/purchasegroup/store-buy/pack-open/
quick-sell/match) inline; async handlers (market list/query/buy/cancel,
move) on the bridge via owned `async move` blocks. build_content_pool
derives the resolvable FIFA∩Core candidate pool from Core content.
market.rs: FIX the load-bearing hazard the FakeEconomy tests missed — the
async market handlers call the BLOCKING reqwest Core client, which panics
(reqwest::blocking::wait::enter) when run while a Tokio runtime is entered.
off_runtime() hops each Core call to a fresh OS thread with no runtime
entered, so blocking is legal. handle_move_items resolver gains `+ Sync`
(future must be Send for the bridge).
tests/economy_integration.rs: economy_full_sequence_through_dispatch drives
the WHOLE cluster through the REAL Server dispatch + bridge against a live
in-process Core (seeded with fifa17 dev content: 100k coins + owned cards),
on a plain OS thread (direct bridge path), over the real blocking
HttpCoreClient — no fakes: Store BUY (pool draw + shape + debit 400 + mint 5),
credits, quick-sell (reverse-resolve + credit), match WIN (+400), market
list->query->buy->query(sold)->second-buy-fails(no double debit), cancel
(cancelled not buyable), move-items, then reopen the durable market/pile
stores from disk (sold + pile persist). Deterministic. start_core_seeded
loads fifa17 dev content so /collection renders real definitions.
Tests: host 68 lib (+4 bridge) + 2 economy_integration + 24 host_test, all
green; adapter unchanged-green; clippy -D warnings + fmt clean.
This commit is contained in:
@@ -102,11 +102,26 @@ fn auction_record(l: &Listing) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
/// Run a BLOCKING closure — the blocking Core client — on a fresh OS thread that
|
||||
/// has NO Tokio runtime entered, then block until it finishes. The market
|
||||
/// handlers are `async` and driven on the shared runtime by the host bridge, but
|
||||
/// `CoreEconomy` is a `reqwest::blocking` client: calling it while a runtime is
|
||||
/// entered panics (`reqwest::blocking::wait::enter`). Hopping to a plain thread
|
||||
/// keeps the Core call off the runtime, so blocking is legal. Cheap relative to
|
||||
/// the network round-trip it guards.
|
||||
fn off_runtime<T, F>(f: F) -> T
|
||||
where
|
||||
F: FnOnce() -> T + Send,
|
||||
T: Send,
|
||||
{
|
||||
std::thread::scope(|s| s.spawn(f).join().expect("off-runtime Core call panicked"))
|
||||
}
|
||||
|
||||
/// Current Core balance as the `credits` field, or 0 if Core is unreachable
|
||||
/// (used only to decorate an already-decided response; the transactional path
|
||||
/// never trusts a fabricated balance).
|
||||
fn credits_or_zero(econ: &dyn CoreEconomy) -> i64 {
|
||||
econ.balance().unwrap_or(0)
|
||||
off_runtime(|| econ.balance()).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// `/auctionhouse` — search (GET), list-for-sale (POST FutISStart), relist (PUT).
|
||||
@@ -280,7 +295,7 @@ pub async fn handle_market_buy(
|
||||
|
||||
// Pre-check funds so an affordability failure is a clean 461, distinct from a
|
||||
// Core transport failure (503). Core's purchase is still the atomic authority.
|
||||
let balance = match econ.balance() {
|
||||
let balance = match off_runtime(|| econ.balance()) {
|
||||
Ok(b) => b,
|
||||
Err(_) => {
|
||||
let _ = store.rollback_reservation(&id).await;
|
||||
@@ -298,7 +313,7 @@ pub async fn handle_market_buy(
|
||||
// Mint the won card into the buyer's club. A listing sells at most once (the
|
||||
// CAS guarantees it), so a deterministic minted id is safe and idempotent.
|
||||
let minted_item_id = format!("market-buy:{id}");
|
||||
match econ.purchase_item(price, &minted_item_id, &listing.card_id) {
|
||||
match off_runtime(|| econ.purchase_item(price, &minted_item_id, &listing.card_id)) {
|
||||
Ok(new_balance) => {
|
||||
// Core has taken the coins and minted the item; finalise the listing.
|
||||
// If completing fails, Core is still authoritative — report success.
|
||||
@@ -336,7 +351,7 @@ pub async fn handle_market_buy(
|
||||
/// never a fabricated move.
|
||||
pub async fn handle_move_items(
|
||||
body: &[u8],
|
||||
resolver: &dyn SquadWireResolver,
|
||||
resolver: &(dyn SquadWireResolver + Sync),
|
||||
pile_store: &PileStore,
|
||||
) -> WireResponse {
|
||||
let b = parse_body(body);
|
||||
|
||||
Reference in New Issue
Block a user