2026-02-25 01:25:42 +08:00
//! Parallel executor for multi-SWQOS submit.
2026-03-08 01:04:11 +08:00
//!
//! - **Pool**: Pre-spawned workers; hot path only enqueues jobs (no per-call tokio::spawn).
//! - **Arc**: Shared data is behind `Arc` so "clone" is just a refcount increment (no data copy).
//! - **Refs**: `build_transaction` takes `&Arc<..>`, `Option<&DurableNonceInfo>`, `Option<&AddressLookupTableAccount>` so the worker passes refs only (zero clone on worker path).
2025-10-06 23:40:27 +08:00
use anyhow ::{ anyhow , Result };
use crossbeam_queue ::ArrayQueue ;
2026-03-08 01:04:11 +08:00
use once_cell ::sync ::OnceCell ;
2025-10-06 23:40:27 +08:00
use solana_hash ::Hash ;
2025-10-07 20:56:02 +08:00
use solana_sdk ::message ::AddressLookupTableAccount ;
2025-10-06 23:40:27 +08:00
use solana_sdk ::{
instruction ::Instruction , pubkey ::Pubkey , signature ::Keypair , signature ::Signature ,
};
2026-03-08 01:04:11 +08:00
use std ::collections ::HashMap ;
use std ::hash ::BuildHasherDefault ;
2025-10-06 23:40:27 +08:00
use std ::sync ::atomic ::{ AtomicBool , AtomicUsize , Ordering };
2025-10-07 23:12:37 +08:00
use std ::{ str ::FromStr , sync ::Arc , time ::Instant };
2025-10-06 23:40:27 +08:00
2026-03-08 01:04:11 +08:00
use fnv ::FnvHasher ;
type FnvHashMap < K , V > = HashMap < K , V , BuildHasherDefault < FnvHasher >> ;
2025-10-06 23:40:27 +08:00
use crate ::{
common ::nonce_cache ::DurableNonceInfo ,
common ::{ GasFeeStrategy , SolanaRpcClient },
swqos ::{ SwqosClient , SwqosType , TradeType },
trading ::{ common ::build_transaction , MiddlewareManager },
};
2026-03-08 01:04:11 +08:00
const SWQOS_POOL_WORKERS : usize = 32 ;
const SWQOS_QUEUE_CAP : usize = 128 ;
/// Shared across all jobs in one batch; built once, cloned as single Arc per job (minimal hot-path clone).
struct SwqosSharedContext {
payer : Arc < Keypair > ,
instructions : Arc < Vec < Instruction >> ,
rpc : Option < Arc < SolanaRpcClient >> ,
address_lookup_table_account : Option < AddressLookupTableAccount > ,
recent_blockhash : Option < Hash > ,
durable_nonce : Option < DurableNonceInfo > ,
middleware_manager : Option < Arc < MiddlewareManager >> ,
protocol_name : & 'static str ,
is_buy : bool ,
wait_transaction_confirmed : bool ,
with_tip : bool ,
collector : Arc < ResultCollector > ,
}
/// One SWQOS submit task; only per-task data + one Arc to shared (reduces hot-path clones).
struct SwqosJob {
shared : Arc < SwqosSharedContext > ,
tip : f64 ,
unit_limit : u32 ,
unit_price : u64 ,
tip_account : Arc < Pubkey > ,
swqos_client : Arc < SwqosClient > ,
swqos_type : SwqosType ,
core_id : Option < core_affinity ::CoreId > ,
use_affinity : bool ,
}
async fn run_one_swqos_job ( job : SwqosJob ) {
let s = & job . shared ;
if job . use_affinity {
if let Some ( cid ) = job . core_id {
core_affinity ::set_for_current ( cid );
}
}
let tip_amount = if s . with_tip { job . tip } else { 0.0 };
let transaction = match build_transaction (
& s . payer ,
s . rpc . as_ref (),
job . unit_limit ,
job . unit_price ,
s . instructions . as_ref (),
s . address_lookup_table_account . as_ref (),
s . recent_blockhash ,
s . middleware_manager . as_ref (),
s . protocol_name ,
s . is_buy ,
job . swqos_type != SwqosType ::Default ,
& job . tip_account ,
tip_amount ,
s . durable_nonce . as_ref (),
)
. await
{
Ok ( tx ) => tx ,
Err ( e ) => {
s . collector . submit ( TaskResult {
success : false ,
signature : Signature ::default (),
error : Some ( e ),
swqos_type : job . swqos_type ,
landed_on_chain : false ,
submit_done_us : crate ::common ::clock ::now_micros (),
});
return ;
}
};
let ( success , err , landed_on_chain ) = match job
. swqos_client
. send_transaction (
if s . is_buy {
TradeType ::Buy
} else {
TradeType ::Sell
},
& transaction ,
s . wait_transaction_confirmed ,
)
. await
{
Ok (()) => ( true , None , true ),
Err ( e ) => {
let landed = is_landed_error ( & e );
( false , Some ( e ), landed )
}
};
let sig = transaction . signatures . first (). copied (). unwrap_or_default ();
s . collector . submit ( TaskResult {
success ,
signature : sig ,
error : err ,
swqos_type : job . swqos_type ,
landed_on_chain ,
submit_done_us : crate ::common ::clock ::now_micros (),
});
}
async fn swqos_worker_loop ( queue : Arc < ArrayQueue < SwqosJob >> ) {
loop {
if let Some ( job ) = queue . pop () {
run_one_swqos_job ( job ). await ;
} else {
tokio ::task ::yield_now (). await ;
}
}
}
static SWQOS_QUEUE : OnceCell < Arc < ArrayQueue < SwqosJob >>> = OnceCell ::new ();
static SWQOS_WORKERS_STARTED : AtomicBool = AtomicBool ::new ( false );
fn ensure_swqos_pool ( queue : Arc < ArrayQueue < SwqosJob >> ) {
if SWQOS_WORKERS_STARTED . swap ( true , Ordering ::AcqRel ) {
return ;
}
for _ in 0 .. SWQOS_POOL_WORKERS {
tokio ::spawn ( swqos_worker_loop ( queue . clone ()));
}
}
2025-10-06 23:40:27 +08:00
#[repr(align(64))]
struct TaskResult {
success : bool ,
signature : Signature ,
2025-11-16 23:46:58 +08:00
error : Option < anyhow ::Error > ,
2026-02-25 01:25:42 +08:00
swqos_type : SwqosType ,
landed_on_chain : bool ,
2026-02-28 20:56:33 +08:00
/// Microsecond timestamp when this task finished (SWQOS returned); for per-SWQOS event→submit timing.
submit_done_us : i64 ,
2025-12-19 08:48:54 +00:00
}
/// Check if an error indicates the transaction landed on-chain (vs network/timeout error)
fn is_landed_error ( error : & anyhow ::Error ) -> bool {
use crate ::swqos ::common ::TradeError ;
// If it's a TradeError with a non-zero code, the tx landed but failed on-chain
if let Some ( trade_error ) = error . downcast_ref ::< TradeError > () {
// Code 500 with "timed out" message means tx never landed
if trade_error . code == 500 && trade_error . message . contains ( "timed out" ) {
return false ;
}
// Any other TradeError means the tx landed (e.g., ExceededSlippage = 6004)
return trade_error . code > 0 ;
}
// Check error message for timeout indication
let msg = error . to_string ();
if msg . contains ( "timed out" ) || msg . contains ( "timeout" ) {
return false ;
}
// Assume other errors might indicate landed tx (be conservative)
false
2025-10-06 23:40:27 +08:00
}
struct ResultCollector {
results : Arc < ArrayQueue < TaskResult >> ,
success_flag : Arc < AtomicBool > ,
2025-12-19 08:48:54 +00:00
landed_failed_flag : Arc < AtomicBool > , // 🔧 Tx landed on-chain but failed (nonce consumed)
2025-10-06 23:40:27 +08:00
completed_count : Arc < AtomicUsize > ,
total_tasks : usize ,
}
impl ResultCollector {
fn new ( capacity : usize ) -> Self {
Self {
results : Arc ::new ( ArrayQueue ::new ( capacity )),
success_flag : Arc ::new ( AtomicBool ::new ( false )),
2025-12-19 08:48:54 +00:00
landed_failed_flag : Arc ::new ( AtomicBool ::new ( false )),
2025-10-06 23:40:27 +08:00
completed_count : Arc ::new ( AtomicUsize ::new ( 0 )),
total_tasks : capacity ,
}
}
fn submit ( & self , result : TaskResult ) {
2026-02-25 01:25:42 +08:00
// ArrayQueue is already synchronized; no extra fence needed
2025-10-07 00:48:33 +08:00
let is_success = result . success ;
2025-12-19 08:48:54 +00:00
let is_landed_failed = result . landed_on_chain && ! result . success ;
2025-10-07 00:48:33 +08:00
2025-10-06 23:40:27 +08:00
let _ = self . results . push ( result );
2025-10-07 00:48:33 +08:00
if is_success {
2026-02-25 01:25:42 +08:00
self . success_flag . store ( true , Ordering ::Release );
2025-12-19 08:48:54 +00:00
} else if is_landed_failed {
// 🔧 Tx landed but failed (e.g., ExceededSlippage) - nonce is consumed, no point waiting
self . landed_failed_flag . store ( true , Ordering ::Release );
2025-10-07 00:48:33 +08:00
}
self . completed_count . fetch_add ( 1 , Ordering ::Release );
2025-10-06 23:40:27 +08:00
}
2026-02-28 20:56:33 +08:00
async fn wait_for_success ( & self ) -> Option < ( bool , Vec < Signature > , Option < anyhow ::Error > , Vec < ( SwqosType , i64 ) > ) > {
2025-10-06 23:40:27 +08:00
let start = Instant ::now ();
2026-02-25 01:25:42 +08:00
let timeout = std ::time ::Duration ::from_secs ( 5 );
let poll_interval = std ::time ::Duration ::from_millis ( 1000 );
2025-10-06 23:40:27 +08:00
loop {
if self . success_flag . load ( Ordering ::Acquire ) {
2025-12-08 01:35:40 +08:00
let mut signatures = Vec ::new ();
let mut has_success = false ;
2026-02-28 20:56:33 +08:00
let mut submit_timings = Vec ::new ();
2025-10-06 23:40:27 +08:00
while let Some ( result ) = self . results . pop () {
2025-12-08 01:35:40 +08:00
signatures . push ( result . signature );
2026-02-28 20:56:33 +08:00
submit_timings . push (( result . swqos_type , result . submit_done_us ));
2025-10-06 23:40:27 +08:00
if result . success {
2025-12-08 01:35:40 +08:00
has_success = true ;
2025-10-06 23:40:27 +08:00
}
}
2025-12-08 01:35:40 +08:00
if has_success && ! signatures . is_empty () {
2026-02-28 20:56:33 +08:00
return Some (( true , signatures , None , submit_timings ));
2025-12-08 01:35:40 +08:00
}
2025-10-06 23:40:27 +08:00
}
2026-02-25 01:25:42 +08:00
// Early exit: if a tx landed but failed (e.g., ExceededSlippage),
2025-12-19 08:48:54 +00:00
// nonce is consumed and other channels can't succeed - return immediately
if self . landed_failed_flag . load ( Ordering ::Acquire ) {
let mut signatures = Vec ::new ();
let mut landed_error = None ;
2026-02-28 20:56:33 +08:00
let mut submit_timings = Vec ::new ();
2025-12-19 08:48:54 +00:00
while let Some ( result ) = self . results . pop () {
signatures . push ( result . signature );
2026-02-28 20:56:33 +08:00
submit_timings . push (( result . swqos_type , result . submit_done_us ));
2025-12-19 08:48:54 +00:00
// Prefer the error from the tx that actually landed
if result . landed_on_chain && result . error . is_some () {
landed_error = result . error ;
}
}
if ! signatures . is_empty () {
2026-02-28 20:56:33 +08:00
return Some (( false , signatures , landed_error , submit_timings ));
2025-12-19 08:48:54 +00:00
}
}
2025-10-06 23:40:27 +08:00
let completed = self . completed_count . load ( Ordering ::Acquire );
2026-02-25 01:25:42 +08:00
if completed >= self . total_tasks {
2025-12-08 01:35:40 +08:00
let mut signatures = Vec ::new ();
let mut last_error = None ;
let mut any_success = false ;
2026-02-28 20:56:33 +08:00
let mut submit_timings = Vec ::new ();
2025-10-06 23:40:27 +08:00
while let Some ( result ) = self . results . pop () {
2025-12-08 01:35:40 +08:00
signatures . push ( result . signature );
2026-02-28 20:56:33 +08:00
submit_timings . push (( result . swqos_type , result . submit_done_us ));
2025-12-08 01:35:40 +08:00
if result . success {
any_success = true ;
}
if result . error . is_some () {
last_error = result . error ;
}
}
if ! signatures . is_empty () {
2026-02-28 20:56:33 +08:00
return Some (( any_success , signatures , last_error , submit_timings ));
2025-10-06 23:40:27 +08:00
}
return None ;
}
if start . elapsed () > timeout {
return None ;
}
2026-02-25 01:25:42 +08:00
tokio ::time ::sleep ( poll_interval ). await ;
2025-10-06 23:40:27 +08:00
}
}
2026-02-28 20:56:33 +08:00
fn get_first ( & self ) -> Option < ( bool , Vec < Signature > , Option < anyhow ::Error > , Vec < ( SwqosType , i64 ) > ) > {
2025-12-08 01:35:40 +08:00
let mut signatures = Vec ::new ();
let mut has_success = false ;
let mut last_error = None ;
2026-02-28 20:56:33 +08:00
let mut submit_timings = Vec ::new ();
2025-12-08 01:35:40 +08:00
while let Some ( result ) = self . results . pop () {
signatures . push ( result . signature );
2026-02-28 20:56:33 +08:00
submit_timings . push (( result . swqos_type , result . submit_done_us ));
2025-12-08 01:35:40 +08:00
if result . success {
has_success = true ;
}
if result . error . is_some () {
last_error = result . error ;
}
}
2026-02-28 20:56:33 +08:00
2025-12-08 01:35:40 +08:00
if ! signatures . is_empty () {
2026-02-28 20:56:33 +08:00
Some (( has_success , signatures , last_error , submit_timings ))
2025-10-06 23:40:27 +08:00
} else {
None
}
}
2026-02-25 01:25:42 +08:00
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有签名。用于「多路提交」时返回多笔签名。
2026-02-28 20:56:33 +08:00
/// 轮询间隔 2ms,避免 50ms 间隔在最后一笔返回时多等几十 ms 拉高 submit 耗时。
async fn wait_for_all_submitted ( & self , timeout_secs : u64 ) -> Option < ( bool , Vec < Signature > , Option < anyhow ::Error > , Vec < ( SwqosType , i64 ) > ) > {
2026-02-25 01:25:42 +08:00
let start = Instant ::now ();
let timeout = std ::time ::Duration ::from_secs ( timeout_secs );
2026-02-28 20:56:33 +08:00
let poll_interval = std ::time ::Duration ::from_millis ( 2 );
2026-02-25 01:25:42 +08:00
while self . completed_count . load ( Ordering ::Acquire ) < self . total_tasks {
if start . elapsed () > timeout {
break ;
}
tokio ::time ::sleep ( poll_interval ). await ;
}
self . get_first ()
}
2025-10-06 23:40:27 +08:00
}
2026-02-25 01:25:42 +08:00
/// Execute trade on multiple SWQOS clients in parallel; returns success flag, all signatures, and last error.
2025-10-06 23:40:27 +08:00
pub async fn execute_parallel (
2026-02-25 01:25:42 +08:00
swqos_clients : & [ Arc < SwqosClient > ],
2025-10-06 23:40:27 +08:00
payer : Arc < Keypair > ,
rpc : Option < Arc < SolanaRpcClient >> ,
instructions : Vec < Instruction > ,
2025-10-07 20:56:02 +08:00
address_lookup_table_account : Option < AddressLookupTableAccount > ,
2025-10-06 23:40:27 +08:00
recent_blockhash : Option < Hash > ,
durable_nonce : Option < DurableNonceInfo > ,
middleware_manager : Option < Arc < MiddlewareManager >> ,
protocol_name : & 'static str ,
is_buy : bool ,
wait_transaction_confirmed : bool ,
with_tip : bool ,
2025-10-07 23:12:37 +08:00
gas_fee_strategy : GasFeeStrategy ,
2026-02-25 01:25:42 +08:00
use_core_affinity : bool ,
2026-02-27 15:00:13 +08:00
check_min_tip : bool ,
2026-02-28 20:56:33 +08:00
) -> Result < ( bool , Vec < Signature > , Option < anyhow ::Error > , Vec < ( SwqosType , i64 ) > ) > {
2025-10-06 23:40:27 +08:00
let _exec_start = Instant ::now ();
if swqos_clients . is_empty () {
return Err ( anyhow! ( "swqos_clients is empty" ));
}
if ! with_tip
&& swqos_clients
. iter ()
. find ( | swqos | matches! ( swqos . get_swqos_type (), SwqosType ::Default ))
. is_none ()
{
return Err ( anyhow! ( "No Rpc Default Swqos configured." ));
}
2026-02-25 01:25:42 +08:00
let cores = core_affinity ::get_core_ids (). unwrap_or_default ();
2025-10-06 23:40:27 +08:00
let instructions = Arc ::new ( instructions );
2026-02-25 01:25:42 +08:00
// Precompute all valid (client, gas config) combinations
2025-10-06 23:40:27 +08:00
let task_configs : Vec < _ > = swqos_clients
. iter ()
. enumerate ()
. filter ( | ( _ , swqos_client ) | {
with_tip || matches! ( swqos_client . get_swqos_type (), SwqosType ::Default )
})
. flat_map ( | ( i , swqos_client ) | {
2026-02-27 15:00:13 +08:00
let swqos_type = swqos_client . get_swqos_type ();
2025-10-07 23:12:37 +08:00
let gas_fee_strategy_configs = gas_fee_strategy . get_strategies ( if is_buy {
2025-10-06 23:40:27 +08:00
TradeType ::Buy
} else {
TradeType ::Sell
});
2026-02-27 15:00:13 +08:00
let check_tip = with_tip && ! matches! ( swqos_type , SwqosType ::Default ) && check_min_tip ;
let min_tip = if check_tip {
swqos_client . min_tip_sol ()
} else {
0.0
};
2025-10-06 23:40:27 +08:00
gas_fee_strategy_configs
. into_iter ()
2026-02-27 15:00:13 +08:00
. filter ( move | config | config . 0 == swqos_type )
. filter ( move | config | {
if check_tip {
2026-02-25 01:25:42 +08:00
if config . 2. tip < min_tip && crate ::common ::sdk_log ::sdk_log_enabled () {
2025-12-01 00:13:34 +08:00
println! (
2026-02-25 01:25:42 +08:00
"⚠️ Config filtered: {:?} tip {} is below minimum required {} " ,
2025-12-01 00:13:34 +08:00
config . 0 , config . 2. tip , min_tip
);
}
2025-11-24 06:03:31 +08:00
config . 2. tip >= min_tip
} else {
true
}
})
2025-10-06 23:40:27 +08:00
. map ( move | config | ( i , swqos_client . clone (), config ))
})
. collect ();
if task_configs . is_empty () {
return Err ( anyhow! ( "No available gas fee strategy configs" ));
}
2025-10-23 00:32:21 +08:00
if is_buy && task_configs . len () > 1 && durable_nonce . is_none () {
2025-10-12 20:30:33 +08:00
return Err ( anyhow! ( "Multiple swqos transactions require durable_nonce to be set." ,));
}
2026-03-08 01:04:11 +08:00
// Task preparation completed: one shared context (clone once per batch), then minimal per-task data.
2025-10-06 23:40:27 +08:00
let collector = Arc ::new ( ResultCollector ::new ( task_configs . len ()));
2026-03-08 01:04:11 +08:00
let shared = Arc ::new ( SwqosSharedContext {
payer ,
instructions ,
rpc ,
address_lookup_table_account ,
recent_blockhash ,
durable_nonce ,
middleware_manager ,
protocol_name ,
is_buy ,
wait_transaction_confirmed ,
with_tip ,
collector : collector . clone (),
});
2025-10-06 23:40:27 +08:00
2026-03-08 01:04:11 +08:00
let queue = SWQOS_QUEUE . get_or_init ( || Arc ::new ( ArrayQueue ::new ( SWQOS_QUEUE_CAP )));
ensure_swqos_pool ( queue . clone ());
2025-10-06 23:40:27 +08:00
2026-03-08 01:04:11 +08:00
{
// Cache tip_account per client (one get_tip_account/from_str per unique client per batch). Dropped before await so future stays Send.
let mut tip_cache : FnvHashMap <* const (), Arc < Pubkey >> =
FnvHashMap ::with_capacity_and_hasher ( task_configs . len (), BuildHasherDefault ::default ());
for ( i , swqos_client , gas_fee_strategy_config ) in task_configs {
let core_id = cores . get ( i % cores . len (). max ( 1 )). copied ();
let swqos_type = swqos_client . get_swqos_type ();
let key = Arc ::as_ptr ( & swqos_client ) as * const ();
let tip_account = match tip_cache . get ( & key ) {
Some ( tip ) => tip . clone (),
None => {
let s = swqos_client . get_tip_account () ? ;
let tip = Arc ::new ( Pubkey ::from_str ( & s ). unwrap_or_default ());
tip_cache . insert ( key , tip . clone ());
tip
2026-02-25 01:25:42 +08:00
}
2026-03-08 01:04:11 +08:00
};
let ( tip , unit_limit , unit_price ) = (
gas_fee_strategy_config . 2. tip ,
gas_fee_strategy_config . 2. cu_limit ,
gas_fee_strategy_config . 2. cu_price ,
);
let job = SwqosJob {
shared : shared . clone (),
tip ,
2025-10-06 23:40:27 +08:00
unit_limit ,
unit_price ,
2026-03-08 01:04:11 +08:00
tip_account ,
swqos_client ,
2026-02-25 01:25:42 +08:00
swqos_type ,
2026-03-08 01:04:11 +08:00
core_id ,
use_affinity : use_core_affinity ,
};
let _ = queue . push ( job );
}
2025-10-06 23:40:27 +08:00
}
2026-03-08 01:04:11 +08:00
// All jobs enqueued (no spawn on hot path)
2025-10-06 23:40:27 +08:00
if ! wait_transaction_confirmed {
2026-02-25 01:25:42 +08:00
const SUBMIT_TIMEOUT_SECS : u64 = 30 ;
2026-02-28 20:56:33 +08:00
let ret = collector
2026-02-25 01:25:42 +08:00
. wait_for_all_submitted ( SUBMIT_TIMEOUT_SECS )
. await
2026-02-28 20:56:33 +08:00
. unwrap_or (( false , vec! [], Some ( anyhow! ( "No SWQOS result within {}s" , SUBMIT_TIMEOUT_SECS )), vec! []));
let ( success , signatures , last_error , submit_timings ) = ret ;
return Ok (( success , signatures , last_error , submit_timings ));
2025-10-06 23:40:27 +08:00
}
if let Some ( result ) = collector . wait_for_success (). await {
2026-02-28 20:56:33 +08:00
let ( success , signatures , last_error , submit_timings ) = result ;
Ok (( success , signatures , last_error , submit_timings ))
2025-10-06 23:40:27 +08:00
} else {
Err ( anyhow! ( "All transactions failed" ))
}
}