Update SWQOS submit behavior

This commit is contained in:
0xfnzero
2026-06-23 04:32:50 +08:00
parent 30df893c30
commit 9af86c8397
48 changed files with 50563 additions and 166 deletions
+27 -26
View File
@@ -47,12 +47,13 @@ const SWQOS_POOL_WORKERS: usize = 18;
const SWQOS_QUEUE_CAP: usize = 128;
const SWQOS_DEDICATED_DEFAULT_THREADS: usize = 18;
const FAST_SUBMIT_RESULT_TIMEOUT: Duration = Duration::from_secs(5);
const FAST_SUBMIT_DRAIN_GRACE: Duration = Duration::from_millis(20);
/// 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>>,
address_lookup_table_account: Option<AddressLookupTableAccount>,
address_lookup_table_accounts: Arc<Vec<AddressLookupTableAccount>>,
recent_blockhash: Option<Hash>,
durable_nonce: Option<DurableNonceInfo>,
middleware_manager: Option<Arc<MiddlewareManager>>,
@@ -92,7 +93,7 @@ async fn run_one_swqos_job(job: SwqosJob) {
job.unit_limit,
job.unit_price,
s.instructions.as_ref(),
s.address_lookup_table_account.as_ref(),
s.address_lookup_table_accounts.as_slice(),
s.recent_blockhash,
s.middleware_manager.as_ref(),
s.protocol_name,
@@ -508,10 +509,11 @@ impl ResultCollector {
}
}
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有签名。用于「多路提交」时返回多笔签名。
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有已返回的签名。
/// 轮询间隔 2ms,避免 50ms 间隔在最后一笔返回时多等几十 ms 拉高 submit 耗时。
/// Re-enabled via `SwapParams.wait_for_all_submits` for callers that confirm
/// externally against a pinned durable nonce and need every submitted sig.
/// Re-enabled via `SwapParams.wait_for_all_submits` for callers that need
/// every submitted signature, either for external monitoring or for
/// executor-level poll-any confirmation after parallel submit.
async fn wait_for_all_submitted(
&self,
timeout_secs: u64,
@@ -525,21 +527,10 @@ impl ResultCollector {
}
tokio::time::sleep(poll_interval).await;
}
// 「不等待链上确认」仍会等各 SWQOS 的 HTTP 回包;主循环在收齐或触达 `timeout_secs` 后结束。
// 若主窗口到时仍有未回包通道,晚到的 TaskResult 若立刻 drain 会丢签名——仅在该路径上拉长 grace。
let all_submitted = self.completed_count.load(Ordering::Acquire) >= self.total_tasks;
if all_submitted {
tokio::task::yield_now().await;
} else {
tokio::time::sleep(Duration::from_millis(600)).await;
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
if start.elapsed() > primary + Duration::from_secs(6) {
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
tokio::time::sleep(Duration::from_millis(120)).await;
}
// Bound the opt-in "all submits" path tightly. A slow relay must not
// delay poll-any confirmation by multiple seconds after the submit
// window; give only a short grace for a just-finished worker to publish.
tokio::time::sleep(FAST_SUBMIT_DRAIN_GRACE).await;
self.get_first()
}
}
@@ -608,7 +599,7 @@ pub async fn execute_parallel(
swqos_clients: &[Arc<SwqosClient>],
payer: Arc<Keypair>,
instructions: Vec<Instruction>,
address_lookup_table_account: Option<AddressLookupTableAccount>,
address_lookup_table_accounts: Vec<AddressLookupTableAccount>,
recent_blockhash: Option<Hash>,
durable_nonce: Option<DurableNonceInfo>,
middleware_manager: Option<Arc<MiddlewareManager>>,
@@ -660,17 +651,13 @@ pub async fn execute_parallel(
return Err(anyhow!("No available gas fee strategy configs"));
}
if is_buy && selected_task_configs.len() > 1 && durable_nonce.is_none() {
return Err(anyhow!("Multiple swqos transactions require durable_nonce to be set.",));
}
// Task preparation completed: one shared context (clone once per batch), then minimal per-task data.
let channel_count = selected_task_configs.len().max(1);
let collector = Arc::new(ResultCollector::new(channel_count));
let shared = Arc::new(SwqosSharedContext {
payer,
instructions,
address_lookup_table_account,
address_lookup_table_accounts: Arc::new(address_lookup_table_accounts),
recent_blockhash,
durable_nonce,
middleware_manager,
@@ -848,4 +835,18 @@ mod tests {
assert_eq!(selected[0].gas_fee_config.2.cu_price, 700_000);
assert_eq!(selected[0].gas_fee_config.2.tip, 0.0);
}
#[tokio::test]
async fn wait_for_all_submitted_timeout_is_bounded() {
let collector = ResultCollector::new(1);
let start = Instant::now();
let result = collector.wait_for_all_submitted(0).await;
assert!(result.is_none());
assert!(
start.elapsed() < Duration::from_millis(150),
"wait_for_all_submitted should not add multi-second grace after timeout"
);
}
}
+9 -8
View File
@@ -96,6 +96,7 @@ impl TradeExecutor for GenericTradeExecutor {
total_start.as_ref().map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
let before_submit_us = (params.log_enabled && crate::common::sdk_log::sdk_log_enabled())
.then(crate::common::clock::now_micros);
let address_lookup_table_accounts = params.address_lookup_table_accounts.clone();
if params.simulate {
let send_start = crate::common::sdk_log::sdk_log_enabled().then(Instant::now);
@@ -103,7 +104,7 @@ impl TradeExecutor for GenericTradeExecutor {
params.rpc,
params.payer,
final_instructions,
params.address_lookup_table_account,
address_lookup_table_accounts,
params.recent_blockhash,
params.durable_nonce,
params.middleware_manager,
@@ -157,16 +158,16 @@ impl TradeExecutor for GenericTradeExecutor {
}
let need_confirm = params.wait_tx_confirmed;
// When the caller confirms externally (need_confirm = false) and opts in
// via SwapParams.wait_for_all_submits, return every route's signature so
// pinned-nonce confirmation can poll all of them.
let wait_for_all_submits = !need_confirm && params.wait_for_all_submits;
// Each SWQOS lane may submit a distinct transaction because relay tips
// can use different accounts, so confirmation must be able to poll every
// returned signature when the caller opts in.
let wait_for_all_submits = params.wait_for_all_submits;
let sender_config = params.sender_concurrency_config();
let result = execute_parallel(
params.swqos_clients.as_slice(),
params.payer,
final_instructions,
params.address_lookup_table_account,
address_lookup_table_accounts,
params.recent_blockhash,
params.durable_nonce,
params.middleware_manager,
@@ -253,7 +254,7 @@ async fn simulate_transaction(
rpc: Option<Arc<SolanaRpcClient>>,
payer: Arc<Keypair>,
instructions: Vec<Instruction>,
address_lookup_table_account: Option<AddressLookupTableAccount>,
address_lookup_table_accounts: Vec<AddressLookupTableAccount>,
recent_blockhash: Option<Hash>,
durable_nonce: Option<DurableNonceInfo>,
middleware_manager: Option<Arc<MiddlewareManager>>,
@@ -288,7 +289,7 @@ async fn simulate_transaction(
unit_limit,
unit_price,
&instructions,
address_lookup_table_account.as_ref(),
address_lookup_table_accounts.as_slice(),
recent_blockhash,
middleware_manager.as_ref(),
protocol_name,
+10 -8
View File
@@ -61,7 +61,7 @@ pub struct SwapParams {
pub output_token_program: Option<Pubkey>,
pub input_amount: Option<u64>,
pub slippage_basis_points: Option<u64>,
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
pub address_lookup_table_accounts: Vec<AddressLookupTableAccount>,
pub recent_blockhash: Option<Hash>,
pub wait_tx_confirmed: bool,
pub protocol_params: DexParamEnum,
@@ -82,13 +82,15 @@ pub struct SwapParams {
pub simulate: bool,
/// Whether to output SDK logs (from TradeConfig.log_enabled).
pub log_enabled: bool,
/// Fast-submit only (`wait_tx_confirmed = false`): when true, wait for every
/// SWQOS route's HTTP submit response so all submitted signatures are
/// returned. Defaults to false (post-4.0.11 behaviour: return after the
/// first route accepts). Set to true when the caller does its own on-chain
/// confirmation against a pinned durable nonce — only one route's tx can
/// land, but the caller cannot know which in advance, so it needs every
/// signature to poll via `getSignatureStatuses`.
/// When true, wait for every SWQOS route's HTTP submit response before
/// returning so the result includes all submitted signatures.
///
/// This is useful when confirmation or external monitoring polls all
/// signatures: each route can submit a distinct transaction because relay
/// tips may use different accounts. With a durable nonce, at most one route
/// can consume the nonce; with a recent blockhash, multiple route variants
/// may be valid, so callers must choose strategy and account state
/// accordingly. Defaults to false for lower submit latency.
pub wait_for_all_submits: bool,
/// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()).
pub use_dedicated_sender_threads: bool,
+6 -6
View File
@@ -55,7 +55,7 @@ impl PreallocatedTxBuilder {
///
/// # 交易版本自动选择
///
/// - **有地址查找表** (`lookup_table = Some`): 使用 `VersionedMessage::V0`
/// - **有地址查找表** (`lookup_tables` 非空): 使用 `VersionedMessage::V0`
/// - 支持地址查找表压缩
/// - 减少交易大小
/// - 需要 RPC 支持 V0
@@ -69,11 +69,11 @@ impl PreallocatedTxBuilder {
///
/// ```rust,ignore
/// // 无查找表 -> Legacy 消息
/// let msg = builder.build_zero_alloc(&payer, &ixs, None, blockhash);
/// let msg = builder.build_zero_alloc(&payer, &ixs, &[], blockhash);
/// assert!(matches!(msg, VersionedMessage::Legacy(_)));
///
/// // 有查找表 -> V0 消息
/// let msg = builder.build_zero_alloc(&payer, &ixs, Some(table_key), blockhash);
/// let msg = builder.build_zero_alloc(&payer, &ixs, &[lookup_table], blockhash);
/// assert!(matches!(msg, VersionedMessage::V0(_)));
/// ```
#[inline(always)]
@@ -81,17 +81,17 @@ impl PreallocatedTxBuilder {
&mut self,
payer: &Pubkey,
instructions: &[Instruction],
address_lookup_table_account: Option<&AddressLookupTableAccount>,
address_lookup_table_accounts: &[AddressLookupTableAccount],
recent_blockhash: Hash,
) -> Result<VersionedMessage> {
self.reset();
self.instructions.extend_from_slice(instructions);
if let Some(alt) = address_lookup_table_account {
if !address_lookup_table_accounts.is_empty() {
let message = v0::Message::try_compile(
payer,
&self.instructions,
std::slice::from_ref(alt),
address_lookup_table_accounts,
recent_blockhash,
)?;
Ok(VersionedMessage::V0(message))