use std::collections::HashSet;
use std::fmt;
use std::sync::{Arc, RwLock as StdRwLock};
use std::time::Duration;
use ractor::rpc::CallResult;
use super::{deadline_after, try_send_envelope_until, Node, NodeError, PeerChannel};
use crate::actor::{KvMessage, ScanMatch, ScanSpec};
use crate::proto::{self, Envelope, EnvelopeMsg, ScanResponse};
use crate::shard::FragmentId;
pub const MAX_SCAN_TOTAL_MATCHES: usize = 100_000;
const SCAN_FRAGMENT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug)]
pub enum ScanError {
FragmentsUnavailable(Vec<FragmentId>),
Backpressure(String),
Actor(String),
ShuttingDown,
}
impl fmt::Display for ScanError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ScanError::FragmentsUnavailable(fragments) => {
write!(f, "no reachable holder for fragments: {fragments:?}")
}
ScanError::Backpressure(reason) => write!(f, "scan shed by backpressure: {reason}"),
ScanError::Actor(reason) => write!(f, "scan actor error: {reason}"),
ScanError::ShuttingDown => write!(f, "node is shutting down"),
}
}
}
impl std::error::Error for ScanError {}
impl Node {
pub async fn scan_bounded(
&self,
spec: &ScanSpec,
) -> Result<Vec<ScanMatch>, crate::query::QueryExecutionError> {
use crate::query::{QueryExecutionError, QueryOptions};
use actordb_query::QueryValue;
let _operation = self
.admit_operation()
.map_err(|_| QueryExecutionError::ShuttingDown)?;
let request_bytes = match &spec.matcher {
crate::actor::KeyMatcher::All => 1024,
crate::actor::KeyMatcher::Prefix(prefix) => {
prefix.capacity().saturating_mul(2).saturating_add(1024)
}
};
let _runtime = self.query_admission.reserve_work(
request_bytes,
QueryOptions::default().max_result_bytes + QueryOptions::default().max_buffer_bytes,
)?;
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(60);
tokio::time::timeout_at(deadline, async {
let mut merged = Vec::new();
let mut retained = 0usize;
let mut retained_payload = 0usize;
let mut visited = 0u64;
for fragment in 0..self.router.fragment_count() {
let mut plan = spec.query_plan(Some(fragment as u32));
plan.limit = Some(spec.effective_limit().min(MAX_SCAN_TOTAL_MATCHES as u64));
if merged.len() == MAX_SCAN_TOTAL_MATCHES {
plan.projections = vec![actordb_query::Projection::Key];
}
let remaining = if merged.len() == MAX_SCAN_TOTAL_MATCHES {
16 * 1024 * 1024usize
} else {
(16 * 1024 * 1024usize).saturating_sub(retained)
};
let options = QueryOptions {
max_result_bytes: remaining,
max_scanned_rows: 1_000_000u64.saturating_sub(visited),
timeout: deadline.saturating_duration_since(tokio::time::Instant::now()),
..Default::default()
};
if options.max_scanned_rows == 0 {
return Err(QueryExecutionError::BudgetExceeded(
"scan visited record limit".into(),
));
}
let (result, work) = self
.query_plan_with_options_and_visits(&plan, options)
.await?;
visited += work;
for row in result.rows {
if merged.len() == MAX_SCAN_TOTAL_MATCHES {
break;
}
let mut cells = row.into_iter();
let Some(QueryValue::Text(key)) = cells.next() else {
return Err(QueryExecutionError::Protocol("scan key projection".into()));
};
let value = match cells.next() {
Some(QueryValue::Bytes(bytes)) => bytes,
None => Vec::new(),
_ => {
return Err(QueryExecutionError::Protocol(
"scan value projection".into(),
))
}
};
retained_payload = retained_payload
.saturating_add(key.capacity())
.saturating_add(value.capacity());
let capacity = if merged.len() == merged.capacity() {
merged.capacity().saturating_mul(2).max(4)
} else {
merged.capacity()
};
retained = retained_payload
.saturating_add(capacity.saturating_mul(std::mem::size_of::<ScanMatch>()));
if retained > 16 * 1024 * 1024 {
return Err(QueryExecutionError::BudgetExceeded(
"scan result storage".into(),
));
}
merged.push(ScanMatch { key, value });
}
}
Ok(merged)
})
.await
.map_err(|_| QueryExecutionError::Timeout)?
}
pub async fn scan(&self, spec: &ScanSpec) -> Result<Vec<ScanMatch>, ScanError> {
use crate::query::QueryExecutionError;
self.scan_bounded(spec).await.map_err(|error| match error {
QueryExecutionError::FragmentsUnavailable(ids) => {
ScanError::FragmentsUnavailable(ids.into_iter().map(FragmentId).collect())
}
QueryExecutionError::Backpressure(reason) => ScanError::Backpressure(reason),
QueryExecutionError::ShuttingDown => ScanError::ShuttingDown,
other => ScanError::Actor(other.to_string()),
})
}
}
pub(super) async fn serve_scan_request(
tx: PeerChannel,
serving: Arc<StdRwLock<HashSet<FragmentId>>>,
router: super::FragmentRouter,
req: proto::ScanRequest,
retention: Arc<crate::query::QueryRetention>,
) {
let fragment = FragmentId(req.fragment);
let req_id = req.req_id;
if !serving.read().unwrap().contains(&fragment) {
send_scan_response(
&tx,
req_id,
Vec::new(),
format!("not serving fragment {fragment}"),
)
.await;
return;
}
let spec = match ScanSpec::from_scan_request(&req) {
Ok(spec) => spec,
Err(error) => {
send_scan_response(
&tx,
req_id,
Vec::new(),
format!("invalid scan spec: {error}"),
)
.await;
return;
}
};
let Some(actor) = router.route_for_fragment(fragment) else {
send_scan_response(
&tx,
req_id,
Vec::new(),
format!("no actor for fragment {fragment}"),
)
.await;
return;
};
let matches = match actor
.call(
|reply| KvMessage::ScanRetained {
spec,
retention: retention.clone(),
reply,
},
Some(SCAN_FRAGMENT_TIMEOUT),
)
.await
{
Ok(CallResult::Success(Ok(matches))) => matches,
Ok(CallResult::Success(Err(shed))) => {
send_scan_response(&tx, req_id, Vec::new(), format!("scan shed: {shed}")).await;
return;
}
_ => {
send_scan_response(
&tx,
req_id,
Vec::new(),
"scan actor call failed".to_string(),
)
.await;
return;
}
};
let proto_matches: Vec<proto::ScanMatchProto> = matches
.into_iter()
.map(|m| proto::ScanMatchProto {
key: m.key,
value: m.value,
})
.collect();
send_scan_response(&tx, req_id, proto_matches, String::new()).await;
}
pub(super) async fn send_scan_response(
tx: &PeerChannel,
req_id: u64,
matches: Vec<proto::ScanMatchProto>,
error: String,
) {
let _ = try_send_envelope_until(
tx,
Envelope {
msg: Some(EnvelopeMsg::ScanResponse(ScanResponse {
req_id,
matches,
error,
})),
},
deadline_after(Duration::from_secs(10)),
)
.await;
}
impl From<ScanError> for NodeError {
fn from(error: ScanError) -> Self {
match error {
ScanError::FragmentsUnavailable(fragments) => {
NodeError::Actor(format!("no reachable holder for fragments: {fragments:?}"))
}
ScanError::Backpressure(reason) => NodeError::Backpressure(reason),
ScanError::Actor(reason) => NodeError::Actor(reason),
ScanError::ShuttingDown => NodeError::ShuttingDown,
}
}
}