Skip to content

Commit 03c17b9

Browse files
authored
feat(cache): admit gateway-fetched blocks locally (#514)
1 parent 0a1de48 commit 03c17b9

9 files changed

Lines changed: 679 additions & 17 deletions

File tree

crates/talon-cache-client/src/worker_client.rs

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ use std::path::Path;
1717
use std::sync::Arc;
1818
use std::time::Duration;
1919

20-
use talon_core::{ObjectId, RequestId, Version};
20+
use talon_core::{BlockId, ObjectId, RequestId, Version};
2121
use talon_transport::frame::{FrameHeader, MsgType, HEADER_LEN};
2222
use talon_transport::{
23-
decode_error_payload, encode_cached_request, encode_delete, encode_put_header, encode_request,
24-
CachedRangeRequest, DataPlaneError, DeleteRequest, Flags, PutRequest, RangeRequest,
25-
MAX_CONTROL_PAYLOAD_LEN,
23+
decode_error_payload, encode_cached_block_put_header, encode_cached_request, encode_delete,
24+
encode_put_header, encode_request, CachedBlockPutRequest, CachedRangeRequest, DataPlaneError,
25+
DeleteRequest, Flags, PutRequest, RangeRequest, MAX_CONTROL_PAYLOAD_LEN,
2626
};
2727
use tokio::io::{AsyncReadExt, AsyncWriteExt};
2828
use tokio::net::TcpStream;
@@ -197,6 +197,74 @@ impl WorkerClient {
197197
}
198198
}
199199

200+
/// Admit one complete, versioned block without asking the worker to access
201+
/// its configured backend.
202+
pub async fn admit_cached_block(
203+
&self,
204+
block: &BlockId,
205+
object_len: u64,
206+
body: &[u8],
207+
) -> Result<(), WorkerError> {
208+
let request_id = RequestId::next();
209+
let header = encode_cached_block_put_header(
210+
request_id.0,
211+
&CachedBlockPutRequest {
212+
block: block.clone(),
213+
object_len,
214+
body_len: body.len() as u64,
215+
},
216+
)?;
217+
match self.admit_exchange(&header, body).await {
218+
Ok(()) => Ok(()),
219+
Err((true, _)) => {
220+
let mut stream = self.pool.fresh(&self.addr).await?;
221+
self.pool
222+
.with_deadline(
223+
"worker admit_cached_block retry",
224+
streamed_put_timeout(body.len() as u64),
225+
async {
226+
stream.write_all(&header).await?;
227+
stream.write_all(body).await?;
228+
stream.flush().await?;
229+
read_range_reply(&mut stream, 0).await.map(|_| ())
230+
},
231+
)
232+
.await?;
233+
self.pool.release(&self.addr, stream);
234+
Ok(())
235+
}
236+
Err((false, error)) => Err(error),
237+
}
238+
}
239+
240+
async fn admit_exchange(&self, header: &[u8], body: &[u8]) -> Result<(), (bool, WorkerError)> {
241+
let (mut stream, reused) = self
242+
.pool
243+
.checkout(&self.addr)
244+
.await
245+
.map_err(|error| (false, WorkerError::from(error)))?;
246+
let result = self
247+
.pool
248+
.with_deadline(
249+
"worker admit_cached_block",
250+
streamed_put_timeout(body.len() as u64),
251+
async {
252+
stream.write_all(header).await?;
253+
stream.write_all(body).await?;
254+
stream.flush().await?;
255+
read_range_reply(&mut stream, 0).await.map(|_| ())
256+
},
257+
)
258+
.await;
259+
match result {
260+
Ok(()) => {
261+
self.pool.release(&self.addr, stream);
262+
Ok(())
263+
}
264+
Err(error) => Err((reused, error)),
265+
}
266+
}
267+
200268
/// One request/response over a pooled-or-fresh connection.
201269
///
202270
/// On success releases the connection for reuse. On error returns
@@ -575,14 +643,50 @@ mod tests {
575643
use std::time::Duration;
576644
use talon_core::Backend;
577645
use talon_transport::{
578-
decode_cached_request, decode_request, encode_error, response_header_ok,
646+
decode_cached_block_put_header, decode_cached_request, decode_request, encode_error,
647+
response_header_ok,
579648
};
580649
use tokio::net::TcpListener;
581650

582651
fn object() -> ObjectId {
583652
ObjectId::new(Backend::Azure, "container", "path/to/blob.bin")
584653
}
585654

655+
#[tokio::test]
656+
async fn cache_admission_sends_exact_versioned_block_and_body() {
657+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
658+
let addr = listener.local_addr().unwrap().to_string();
659+
let block = BlockId::new(object(), 8, 8, Version::new("etag-v2"));
660+
let expected = block.clone();
661+
tokio::spawn(async move {
662+
let (mut socket, _) = listener.accept().await.unwrap();
663+
let mut header_bytes = [0_u8; HEADER_LEN];
664+
socket.read_exact(&mut header_bytes).await.unwrap();
665+
let header = FrameHeader::decode(&header_bytes).unwrap();
666+
assert_eq!(header.msg_type, MsgType::AdmitCachedBlock);
667+
let mut payload = vec![0_u8; header.length as usize];
668+
socket.read_exact(&mut payload).await.unwrap();
669+
let mut frame = header_bytes.to_vec();
670+
frame.extend_from_slice(&payload);
671+
let (_, request) = decode_cached_block_put_header(&frame).unwrap();
672+
assert_eq!(request.block, expected);
673+
assert_eq!(request.object_len, 13);
674+
assert_eq!(request.body_len, 5);
675+
let mut body = vec![0_u8; 5];
676+
socket.read_exact(&mut body).await.unwrap();
677+
assert_eq!(body, b"tail!");
678+
socket
679+
.write_all(&response_header_ok(header.request_id, 0))
680+
.await
681+
.unwrap();
682+
});
683+
684+
WorkerClient::new(addr)
685+
.admit_cached_block(&block, 13, b"tail!")
686+
.await
687+
.unwrap();
688+
}
689+
586690
/// What a mock write-worker records: the object and body it received.
587691
type RecordedPut = Arc<std::sync::Mutex<Option<(ObjectId, Vec<u8>)>>>;
588692

crates/talon-transport/src/data.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
//! module only handles the request encode/decode and the response header shape.
1313
1414
use serde::{Deserialize, Serialize};
15-
use talon_core::{ObjectId, Version};
15+
use talon_core::{BlockId, ObjectId, Version};
1616

1717
use crate::frame::{Flags, FrameError, FrameHeader, MsgType, HEADER_LEN};
1818

@@ -102,6 +102,21 @@ pub struct PutRequest {
102102
pub body_len: u64,
103103
}
104104

105+
/// A request to admit one complete origin-fetched block into the local cache.
106+
///
107+
/// The raw block bytes (`body_len` of them) follow this framed header. The
108+
/// worker validates the block geometry against `object_len` and never accesses
109+
/// its backend while handling this operation.
110+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111+
pub struct CachedBlockPutRequest {
112+
/// Exact versioned cache identity of the block.
113+
pub block: BlockId,
114+
/// Authoritative total object length observed by the gateway.
115+
pub object_len: u64,
116+
/// Number of raw block bytes following this header.
117+
pub body_len: u64,
118+
}
119+
105120
/// A client→worker request to delete an object (#226). No body follows.
106121
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107122
pub struct DeleteRequest {
@@ -121,6 +136,9 @@ pub enum DataError {
121136
/// A non-`Put` frame was handed to the put codec.
122137
#[error("expected a Put frame, got {0:?}")]
123138
NotPut(MsgType),
139+
/// A non-`AdmitCachedBlock` frame was handed to the admission codec.
140+
#[error("expected an AdmitCachedBlock frame, got {0:?}")]
141+
NotCachedBlockPut(MsgType),
124142
/// A non-`Delete` frame was handed to the delete codec.
125143
#[error("expected a Delete frame, got {0:?}")]
126144
NotDelete(MsgType),
@@ -234,6 +252,39 @@ pub fn decode_put_header(buf: &[u8]) -> Result<(FrameHeader, PutRequest), DataEr
234252
Ok((header, req))
235253
}
236254

255+
/// Encode a cache-admission header. The caller writes exactly `body_len` raw
256+
/// bytes after the returned frame.
257+
pub fn encode_cached_block_put_header(
258+
request_id: u32,
259+
req: &CachedBlockPutRequest,
260+
) -> Result<Vec<u8>, DataError> {
261+
let body = bincode::serialize(req)?;
262+
let header = FrameHeader::new(MsgType::AdmitCachedBlock, request_id, body.len() as u32);
263+
let mut buf = Vec::with_capacity(HEADER_LEN + body.len());
264+
buf.extend_from_slice(&header.encode());
265+
buf.extend_from_slice(&body);
266+
Ok(buf)
267+
}
268+
269+
/// Decode a cache-admission header (without its trailing raw body).
270+
pub fn decode_cached_block_put_header(
271+
buf: &[u8],
272+
) -> Result<(FrameHeader, CachedBlockPutRequest), DataError> {
273+
let header = FrameHeader::decode(buf)?;
274+
if header.msg_type != MsgType::AdmitCachedBlock {
275+
return Err(DataError::NotCachedBlockPut(header.msg_type));
276+
}
277+
let declared = header.length as usize;
278+
let body = &buf[HEADER_LEN..];
279+
if body.len() != declared {
280+
return Err(DataError::LengthMismatch {
281+
declared,
282+
actual: body.len(),
283+
});
284+
}
285+
Ok((header, bincode::deserialize(body)?))
286+
}
287+
237288
/// Encode a [`DeleteRequest`] into `header || bincode(req)` (no body follows).
238289
pub fn encode_delete(request_id: u32, req: &DeleteRequest) -> Result<Vec<u8>, DataError> {
239290
let body = bincode::serialize(req)?;
@@ -457,6 +508,21 @@ mod tests {
457508
assert!((header.length as usize) < 1024);
458509
}
459510

511+
#[test]
512+
fn cached_block_put_header_round_trips() {
513+
let request = CachedBlockPutRequest {
514+
block: BlockId::new(obj(), 8, 8, Version::new("etag-v3")),
515+
object_len: 13,
516+
body_len: 5,
517+
};
518+
let encoded = encode_cached_block_put_header(43, &request).unwrap();
519+
let (header, decoded) = decode_cached_block_put_header(&encoded).unwrap();
520+
assert_eq!(header.msg_type, MsgType::AdmitCachedBlock);
521+
assert_eq!(header.request_id, 43);
522+
assert_eq!(decoded, request);
523+
assert!((header.length as usize) < 1024);
524+
}
525+
460526
#[test]
461527
fn delete_round_trips() {
462528
let req = DeleteRequest { object: obj() };

crates/talon-transport/src/frame.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ pub enum MsgType {
6161
/// Read a versioned range only when every requested byte is resident.
6262
/// This operation must never access the worker backend.
6363
GetCachedRange = 6,
64+
/// Admit one complete, versioned block into the worker cache without
65+
/// accessing the configured backend.
66+
AdmitCachedBlock = 7,
6467
}
6568

6669
impl MsgType {
@@ -74,6 +77,7 @@ impl MsgType {
7477
4 => MsgType::Ping,
7578
5 => MsgType::Delete,
7679
6 => MsgType::GetCachedRange,
80+
7 => MsgType::AdmitCachedBlock,
7781
other => return Err(FrameError::UnknownMsgType(other)),
7882
})
7983
}
@@ -215,6 +219,7 @@ mod tests {
215219
MsgType::Ping,
216220
MsgType::Delete,
217221
MsgType::GetCachedRange,
222+
MsgType::AdmitCachedBlock,
218223
]
219224
.into_iter()
220225
.enumerate()

crates/talon-transport/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ pub use codec::{
2323
CONTROL_SCHEMA_VERSION, MIN_CONTROL_SCHEMA_VERSION,
2424
};
2525
pub use data::{
26-
decode_cached_request, decode_delete, decode_error_payload, decode_put_header, decode_request,
27-
encode_cached_request, encode_delete, encode_error, encode_put_header, encode_request,
28-
encode_typed_error, response_header_ok, CachedRangeRequest, DataError, DataErrorCode,
26+
decode_cached_block_put_header, decode_cached_request, decode_delete, decode_error_payload,
27+
decode_put_header, decode_request, encode_cached_block_put_header, encode_cached_request,
28+
encode_delete, encode_error, encode_put_header, encode_request, encode_typed_error,
29+
response_header_ok, CachedBlockPutRequest, CachedRangeRequest, DataError, DataErrorCode,
2930
DataPlaneError, DeleteRequest, PutRequest, RangeRequest,
3031
};
3132
pub use frame::{Flags, FrameError, FrameHeader, MsgType, HEADER_LEN, MAGIC, PROTOCOL_VERSION};

crates/talon-transport/src/limits.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ pub fn max_payload_for(msg_type: MsgType) -> u32 {
4848
match msg_type {
4949
MsgType::Control => MAX_CONTROL_PAYLOAD_LEN,
5050
MsgType::Ping => MAX_PING_PAYLOAD_LEN,
51-
MsgType::Put | MsgType::Delete | MsgType::GetCachedRange => MAX_CONTROL_PAYLOAD_LEN,
51+
MsgType::Put | MsgType::Delete | MsgType::GetCachedRange | MsgType::AdmitCachedBlock => {
52+
MAX_CONTROL_PAYLOAD_LEN
53+
}
5254
MsgType::Get | MsgType::GetRange => MAX_PAYLOAD_LEN,
5355
}
5456
}

crates/talon-worker/src/main.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,6 +1367,97 @@ mod tests {
13671367

13681368
drop(client);
13691369
server.await.unwrap();
1370+
1371+
std::fs::remove_dir_all(root).ok();
1372+
}
1373+
1374+
#[tokio::test]
1375+
async fn tokio_conn_admits_a_complete_block_without_origin_access() {
1376+
use talon_core::BlockId;
1377+
use talon_transport::data::{
1378+
encode_cached_block_put_header, CachedBlockPutRequest, CachedRangeRequest,
1379+
};
1380+
1381+
let (worker, observability, _, root) = test_worker();
1382+
observability.readiness().set_backend_ready(true);
1383+
observability.readiness().set_store_ready(true);
1384+
observability.readiness().set_control_registered(true);
1385+
let object = ObjectId::new(talon_core::Backend::Azure, "c", "admitted");
1386+
let block = BlockId::new(object.clone(), 0, 8, Version::new("origin-v2"));
1387+
let request = CachedBlockPutRequest {
1388+
block,
1389+
object_len: 8,
1390+
body_len: 8,
1391+
};
1392+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1393+
let address = listener.local_addr().unwrap();
1394+
let server_worker = Arc::clone(&worker);
1395+
let server_observability = Arc::clone(&observability);
1396+
let server = tokio::spawn(async move {
1397+
let (stream, _) = listener.accept().await.unwrap();
1398+
handle_conn(stream, server_worker, server_observability)
1399+
.await
1400+
.unwrap();
1401+
});
1402+
1403+
let mut client = TcpStream::connect(address).await.unwrap();
1404+
client
1405+
.write_all(&encode_cached_block_put_header(7, &request).unwrap())
1406+
.await
1407+
.unwrap();
1408+
client.write_all(b"gateway!").await.unwrap();
1409+
let mut reply = [0_u8; HEADER_LEN];
1410+
client.read_exact(&mut reply).await.unwrap();
1411+
let reply = FrameHeader::decode(&reply).unwrap();
1412+
assert_eq!(reply.length, 0);
1413+
assert!(!reply.flags.contains(talon_transport::Flags::ERROR));
1414+
assert_eq!(
1415+
worker
1416+
.serve_cached(&CachedRangeRequest {
1417+
object: object.clone(),
1418+
version: Version::new("origin-v2"),
1419+
offset: 0,
1420+
len: 8,
1421+
})
1422+
.await
1423+
.unwrap(),
1424+
Bytes::from_static(b"gateway!")
1425+
);
1426+
drop(client);
1427+
server.await.unwrap();
1428+
1429+
let truncated = CachedBlockPutRequest {
1430+
block: BlockId::new(object.clone(), 0, 8, Version::new("truncated")),
1431+
object_len: 8,
1432+
body_len: 8,
1433+
};
1434+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1435+
let address = listener.local_addr().unwrap();
1436+
let server_worker = Arc::clone(&worker);
1437+
let server_observability = Arc::clone(&observability);
1438+
let server = tokio::spawn(async move {
1439+
let (stream, _) = listener.accept().await.unwrap();
1440+
handle_conn(stream, server_worker, server_observability)
1441+
.await
1442+
.is_err()
1443+
});
1444+
let mut client = TcpStream::connect(address).await.unwrap();
1445+
client
1446+
.write_all(&encode_cached_block_put_header(8, &truncated).unwrap())
1447+
.await
1448+
.unwrap();
1449+
client.write_all(b"short").await.unwrap();
1450+
client.shutdown().await.unwrap();
1451+
assert!(server.await.unwrap());
1452+
assert!(worker
1453+
.serve_cached(&CachedRangeRequest {
1454+
object,
1455+
version: Version::new("truncated"),
1456+
offset: 0,
1457+
len: 1,
1458+
})
1459+
.await
1460+
.is_err());
13701461
std::fs::remove_dir_all(root).ok();
13711462
}
13721463

0 commit comments

Comments
 (0)