Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 9 additions & 1 deletion forester/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ pub struct StartArgs {
)]
pub priority_fee_microlamports: Option<u64>,

#[arg(long, env = "RPC_POOL_SIZE", default_value = "100")]
#[arg(long, env = "RPC_POOL_SIZE", default_value = "32")]
pub rpc_pool_size: u32,

#[arg(long, env = "RPC_POOL_CONNECTION_TIMEOUT_SECS", default_value = "15")]
Expand Down Expand Up @@ -303,6 +303,14 @@ pub struct StartArgs {
)]
pub enable_v1_multi_nullify: bool,

#[arg(
long,
env = "ENABLE_V1_PRESORT",
help = "Fetch queue leaf indices from the indexer (get_queue_leaf_indices) to pre-sort V1 work items for better dedup grouping. Requires an indexer that implements the endpoint. Best-effort; disabled by default.",
default_value = "false"
)]
pub enable_v1_presort: bool,

#[arg(
long,
env = "WORK_ITEM_BATCH_SIZE",
Expand Down
7 changes: 7 additions & 0 deletions forester/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ pub struct ForesterConfig {
/// Enable nullify_state_v1_multi instruction for batching 2-4 V1 state nullifications.
/// Requires lookup_table_address to be set.
pub enable_v1_multi_nullify: bool,
/// Enable the get_queue_leaf_indices pre-sort path for V1 work items.
/// Best-effort optimization; disabled by default. Only enable against an indexer
/// that implements the endpoint.
pub enable_v1_presort: bool,
/// Number of queue items to process per batch cycle. Default: 50.
pub work_item_batch_size: usize,
}
Expand Down Expand Up @@ -431,6 +435,7 @@ impl ForesterConfig {
.transpose()?,
min_queue_items: args.min_queue_items,
enable_v1_multi_nullify: args.enable_v1_multi_nullify,
enable_v1_presort: args.enable_v1_presort,
work_item_batch_size: args.work_item_batch_size.unwrap_or(50) as usize,
})
}
Expand Down Expand Up @@ -488,6 +493,7 @@ impl ForesterConfig {
lookup_table_address: None,
min_queue_items: None,
enable_v1_multi_nullify: false,
enable_v1_presort: false,
work_item_batch_size: 50,
})
}
Expand All @@ -511,6 +517,7 @@ impl Clone for ForesterConfig {
lookup_table_address: self.lookup_table_address,
min_queue_items: self.min_queue_items,
enable_v1_multi_nullify: self.enable_v1_multi_nullify,
enable_v1_presort: self.enable_v1_presort,
work_item_batch_size: self.work_item_batch_size,
}
}
Expand Down
24 changes: 17 additions & 7 deletions forester/src/epoch_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2265,8 +2265,14 @@ impl<R: Rpc + Indexer> EpochManager<R> {

let mut estimated_slot = self.slot_tracker.estimated_current_slot();

// Polling interval for checking queue
const POLL_INTERVAL: Duration = Duration::from_millis(200);
// Adaptive queue polling: start responsive, then back off (capped) while the
// queue has nothing ready to process, and reset to the minimum as soon as work
// is found. A fixed 200ms poll made idle V2 trees re-fetch the queue ~5x/sec for
// the whole eligible window, which is the dominant source of wasted RPC/indexer
// load (and can exhaust a shared RPC credit budget).
const POLL_INTERVAL_MIN: Duration = Duration::from_millis(200);
const POLL_INTERVAL_MAX: Duration = Duration::from_secs(10);
let mut poll_interval = POLL_INTERVAL_MIN;
Comment thread
sergeytimoshin marked this conversation as resolved.

'inner_processing_loop: loop {
if estimated_slot >= forester_slot_details.end_solana_slot {
Expand Down Expand Up @@ -2334,9 +2340,12 @@ impl<R: Rpc + Indexer> EpochManager<R> {
processing_start_time.elapsed(),
)
.await;
// Work found: stay responsive while the queue drains.
poll_interval = POLL_INTERVAL_MIN;
} else {
// No items to process, wait before polling again
tokio::time::sleep(POLL_INTERVAL).await;
// Nothing ready: wait, then back off (capped) to avoid hammering RPC.
tokio::time::sleep(poll_interval).await;
poll_interval = (poll_interval * 2).min(POLL_INTERVAL_MAX);
Comment on lines +2366 to +2367

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cap idle V2 polling to the light-slot budget

When a V2 tree is idle early in its eligible light slot, this sleep backs off without considering how much of the light slot remains. I checked the protocol defaults (programs/registry/src/protocol_config/state.rs): local/default slot_length is 10 Solana slots (~4s) and testnet is 60 (~24s), so after several empty polls the forester can sleep 6.4s/10s, wake after forester_slot_details.end_solana_slot, and skip work that arrived during the sleep until a later eligibility window. Cap the sleep/backoff by the remaining light-slot time or keep it well below the slot length so late-arriving V2 queue items can still be processed in the current slot.

Useful? React with 👍 / 👎.

}
}
Err(e) => {
Expand All @@ -2350,7 +2359,8 @@ impl<R: Rpc + Indexer> EpochManager<R> {
error = ?e,
"V2 processing failed for tree"
);
tokio::time::sleep(POLL_INTERVAL).await;
tokio::time::sleep(poll_interval).await;
poll_interval = (poll_interval * 2).min(POLL_INTERVAL_MAX);
}
}

Expand Down Expand Up @@ -3062,8 +3072,7 @@ impl<R: Rpc + Indexer> EpochManager<R> {
} else {
None
},
enable_presort: self.config.enable_v1_multi_nullify
&& !self.address_lookup_tables.is_empty(),
enable_presort: self.config.enable_v1_presort,
work_item_batch_size: self.config.work_item_batch_size,
};

Expand Down Expand Up @@ -4730,6 +4739,7 @@ mod tests {
lookup_table_address: None,
min_queue_items: None,
enable_v1_multi_nullify: false,
enable_v1_presort: false,
work_item_batch_size: 50,
}
}
Expand Down
15 changes: 12 additions & 3 deletions sdk-libs/client/src/indexer/photon_indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,18 @@ impl PhotonIndexer {
}
Err(e) => {
let is_retryable = match &e {
IndexerError::ApiError(_) => {
warn!("API Error: {}", e);
true
IndexerError::ApiError(msg) => {
// A 404 / "method not found" means the indexer does not implement
// this endpoint; retrying cannot succeed and only burns the backoff
// budget (up to ~44s with the default config). Treat it as non-retryable.
let lower = msg.to_lowercase();
if lower.contains("method not found") || lower.contains("status 404") {
warn!("Non-retryable API error (endpoint not available): {}", e);
false
} else {
warn!("API Error: {}", e);
true
}
}
IndexerError::PhotonError {
context: _,
Expand Down
Loading