use bytes::{Bytes, BytesMut};
use tokio::sync::mpsc;
use tonic::Streaming;
use tracing::{debug, trace, warn};
use crate::client::WorkerClient;
use crate::error::{Error, Result};
use crate::metrics::name;
use crate::proto::grpc::block::{ReadRequest, ReadResponse};
use crate::proto::proto::dataserver::OpenUfsBlockOptions;
pub struct GrpcBlockReader {
block_id: i64,
offset: i64,
length: i64,
bytes_received: i64,
request_tx: mpsc::Sender<ReadRequest>,
response_rx: Streaming<ReadResponse>,
}
#[derive(Debug, PartialEq, Eq)]
enum ChunkAction {
Eof,
KeepReading,
Deliver(Bytes),
}
fn classify_response(resp: Option<ReadResponse>) -> ChunkAction {
match resp {
None => ChunkAction::Eof,
Some(r) => {
let data = r.chunk.and_then(|c| c.data).unwrap_or_default();
if data.is_empty() {
ChunkAction::KeepReading
} else {
ChunkAction::Deliver(Bytes::from(data))
}
}
}
}
fn check_positioned_read_complete(block_id: i64, bytes_received: i64, length: i64) -> Result<()> {
if bytes_received < length {
return Err(Error::Internal {
message: format!(
"short positioned read on block {}: received {} of {} bytes \
(server half-closed early)",
block_id, bytes_received, length
),
source: None,
});
}
Ok(())
}
impl GrpcBlockReader {
pub async fn open(
worker: &WorkerClient,
block_id: i64,
offset: i64,
length: i64,
chunk_size: i64,
open_ufs_block_options: Option<OpenUfsBlockOptions>,
) -> Result<Self> {
let (request_tx, response_rx) = worker
.read_block(block_id, offset, length, chunk_size, open_ufs_block_options)
.await?;
crate::metrics::gauge(name::CLIENT_BLOCKS_READ_IN_PROGRESS)
.set(crate::metrics::gauge(name::CLIENT_BLOCKS_READ_IN_PROGRESS).get() + 1);
debug!(
block_id = block_id,
offset = offset,
length = length,
"opened GrpcBlockReader"
);
Ok(Self {
block_id,
offset,
length,
bytes_received: 0,
request_tx,
response_rx,
})
}
pub async fn read_chunk(&mut self) -> Result<Option<Bytes>> {
if self.bytes_received >= self.length {
return Ok(None);
}
loop {
match classify_response(self.response_rx.message().await?) {
ChunkAction::Eof => {
debug!(
block_id = self.block_id,
bytes_received = self.bytes_received,
"stream ended before all expected data received"
);
return Ok(None);
}
ChunkAction::KeepReading => {
trace!(
block_id = self.block_id,
bytes_received = self.bytes_received,
expected = self.length,
"received empty data frame, awaiting next chunk"
);
continue;
}
ChunkAction::Deliver(data) => {
self.bytes_received += data.len() as i64;
trace!(
block_id = self.block_id,
chunk_len = data.len(),
total_received = self.bytes_received,
"received chunk"
);
crate::metrics::counter(name::CLIENT_BYTES_READ_LOCAL).inc(data.len() as i64);
let ack = ReadRequest {
offset_received: Some(self.offset + self.bytes_received),
..Default::default()
};
if self.request_tx.send(ack).await.is_err() {
warn!(
block_id = self.block_id,
"ACK channel closed (read may be complete)"
);
}
return Ok(Some(data));
}
}
}
}
pub async fn read_all(&mut self) -> Result<Bytes> {
let mut buf = BytesMut::with_capacity(self.length as usize);
while let Some(chunk) = self.read_chunk().await? {
buf.extend_from_slice(&chunk);
}
crate::metrics::counter(name::CLIENT_BLOCKS_READ_TOTAL).inc(1);
crate::metrics::gauge(name::CLIENT_BLOCKS_READ_IN_PROGRESS)
.set((crate::metrics::gauge(name::CLIENT_BLOCKS_READ_IN_PROGRESS).get() - 1).max(0));
check_positioned_read_complete(self.block_id, self.bytes_received, self.length)?;
Ok(buf.freeze())
}
pub fn block_id(&self) -> i64 {
self.block_id
}
pub fn bytes_received(&self) -> i64 {
self.bytes_received
}
pub fn is_complete(&self) -> bool {
self.bytes_received >= self.length
}
pub async fn positioned_read(
worker: &WorkerClient,
block_id: i64,
offset: i64,
length: i64,
chunk_size: i64,
open_ufs_block_options: Option<OpenUfsBlockOptions>,
) -> Result<Bytes> {
let (request_tx, response_rx) = worker
.read_block_positioned(block_id, offset, length, chunk_size, open_ufs_block_options)
.await?;
debug!(
block_id = block_id,
offset = offset,
length = length,
"positioned_read: opened position_short stream"
);
let mut reader = Self {
block_id,
offset,
length,
bytes_received: 0,
request_tx,
response_rx,
};
reader.read_all().await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proto::grpc::block::Chunk;
#[test]
fn metrics_counter_accessible() {
let _counter = crate::metrics::counter(name::CLIENT_BYTES_READ_LOCAL);
}
#[test]
fn classify_response_none_is_eof() {
assert_eq!(classify_response(None), ChunkAction::Eof);
}
#[test]
fn classify_response_no_chunk_is_keep_reading() {
let resp = ReadResponse {
chunk: None,
..Default::default()
};
assert_eq!(classify_response(Some(resp)), ChunkAction::KeepReading);
}
#[test]
fn classify_response_empty_chunk_is_keep_reading() {
let resp = ReadResponse {
chunk: Some(Chunk {
data: Some(Vec::new()),
..Default::default()
}),
..Default::default()
};
assert_eq!(classify_response(Some(resp)), ChunkAction::KeepReading);
}
#[test]
fn classify_response_chunk_with_none_data_is_keep_reading() {
let resp = ReadResponse {
chunk: Some(Chunk {
data: None,
..Default::default()
}),
..Default::default()
};
assert_eq!(classify_response(Some(resp)), ChunkAction::KeepReading);
}
#[test]
fn classify_response_data_is_delivered() {
let payload = b"hello world".to_vec();
let resp = ReadResponse {
chunk: Some(Chunk {
data: Some(payload.clone()),
..Default::default()
}),
..Default::default()
};
match classify_response(Some(resp)) {
ChunkAction::Deliver(b) => assert_eq!(b.as_ref(), payload.as_slice()),
other => panic!("expected Deliver, got {:?}", other),
}
}
#[test]
fn check_positioned_read_complete_short_read_errors() {
let err = check_positioned_read_complete(
16777216, 1024, 4096,
)
.unwrap_err();
let msg = format!("{}", err);
assert!(
msg.contains("short positioned read on block 16777216"),
"expected short-read message, got: {}",
msg
);
assert!(
msg.contains("received 1024 of 4096"),
"expected received/length pair in message, got: {}",
msg
);
}
#[test]
fn check_positioned_read_complete_full_read_ok() {
assert!(check_positioned_read_complete(1, 4096, 4096).is_ok());
}
#[test]
fn check_positioned_read_complete_over_read_ok() {
assert!(check_positioned_read_complete(1, 5000, 4096).is_ok());
}
}