Update SWQOS submit behavior
This commit is contained in:
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user