Skip to content

Commit 02cc378

Browse files
committed
feat(localnet): add recover points
1 parent d99fb2a commit 02cc378

14 files changed

Lines changed: 673 additions & 30 deletions

File tree

crates/acton-localnet-ui/public/openapi/acton-localnet-control.openapi.json

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,45 @@
233233
}
234234
}
235235
},
236+
"/acton_snapshot": {
237+
"post": {
238+
"tags": ["Localnet Control"],
239+
"operationId": "createRecoveryPoint",
240+
"summary": "Create runtime recovery point",
241+
"description": "Creates an in-memory recovery point for the current localnet state. Recovery points are process-local and are not persisted across restart.",
242+
"responses": {
243+
"200": {
244+
"$ref": "#/components/responses/RecoveryPointResponse"
245+
}
246+
}
247+
}
248+
},
249+
"/acton_revert": {
250+
"post": {
251+
"tags": ["Localnet Control"],
252+
"operationId": "revertRecoveryPoint",
253+
"summary": "Revert runtime recovery point",
254+
"description": "Restores a previously created recovery point and invalidates that point plus every newer recovery point.",
255+
"requestBody": {
256+
"required": true,
257+
"content": {
258+
"application/json": {
259+
"schema": {
260+
"$ref": "#/components/schemas/RevertRecoveryPointRequest"
261+
},
262+
"example": {
263+
"id": 1
264+
}
265+
}
266+
}
267+
},
268+
"responses": {
269+
"200": {
270+
"$ref": "#/components/responses/RecoveryPointResponse"
271+
}
272+
}
273+
}
274+
},
236275
"/acton_setShardAccount": {
237276
"post": {
238277
"tags": ["Localnet Control"],
@@ -721,6 +760,28 @@
721760
}
722761
}
723762
},
763+
"RecoveryPointResponse": {
764+
"description": "Successful runtime recovery point response.",
765+
"content": {
766+
"application/json": {
767+
"schema": {
768+
"allOf": [
769+
{
770+
"$ref": "#/components/schemas/SuccessEnvelope"
771+
},
772+
{
773+
"type": "object",
774+
"properties": {
775+
"result": {
776+
"$ref": "#/components/schemas/RecoveryPointResult"
777+
}
778+
}
779+
}
780+
]
781+
}
782+
}
783+
}
784+
},
724785
"NetworkConditionsResponse": {
725786
"description": "Successful simulated network conditions response.",
726787
"content": {
@@ -1152,6 +1213,32 @@
11521213
}
11531214
}
11541215
},
1216+
"RevertRecoveryPointRequest": {
1217+
"type": "object",
1218+
"required": ["id"],
1219+
"properties": {
1220+
"id": {
1221+
"type": "integer",
1222+
"minimum": 1,
1223+
"description": "Recovery point id returned by /acton_snapshot."
1224+
}
1225+
}
1226+
},
1227+
"RecoveryPointResult": {
1228+
"type": "object",
1229+
"required": ["id", "block_seqno"],
1230+
"properties": {
1231+
"id": {
1232+
"type": "integer",
1233+
"minimum": 1
1234+
},
1235+
"block_seqno": {
1236+
"type": "integer",
1237+
"minimum": 0,
1238+
"description": "Latest block seqno captured by the recovery point."
1239+
}
1240+
}
1241+
},
11551242
"IncreaseTimeRequest": {
11561243
"type": "object",
11571244
"required": ["seconds"],

crates/ton-localnet/src/localnet.rs

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::executor::TvmEmulatorAdapter;
22
use crate::node::{Node, NodeClockInfo, StateSource};
3+
use crate::node_snapshot::NodeStateSnapshot;
34
use crate::storage;
45
use crate::storage::{AccountStatus, BlockMeta, MsgMeta, TransactionInfo};
56
use crate::streaming::StreamingCommitEvent;
@@ -181,6 +182,12 @@ pub struct LocalnetMineResult {
181182
pub blocks: Vec<LocalnetBlockId>,
182183
}
183184

185+
#[derive(Debug, Serialize, Deserialize, Clone)]
186+
pub struct LocalnetRecoveryPointResult {
187+
pub id: u64,
188+
pub block_seqno: Seqno,
189+
}
190+
184191
#[derive(Debug, Serialize, Deserialize, Clone)]
185192
pub struct LocalnetBlockHeader {
186193
pub id: LocalnetBlockId,
@@ -408,6 +415,13 @@ pub(crate) enum Request {
408415
path: String,
409416
resp: oneshot::Sender<anyhow::Result<()>>,
410417
},
418+
CreateRecoveryPoint {
419+
resp: oneshot::Sender<anyhow::Result<LocalnetRecoveryPointResult>>,
420+
},
421+
RevertRecoveryPoint {
422+
id: u64,
423+
resp: oneshot::Sender<anyhow::Result<LocalnetRecoveryPointResult>>,
424+
},
411425
MineBlocks {
412426
count: u32,
413427
resp: oneshot::Sender<anyhow::Result<LocalnetMineResult>>,
@@ -435,6 +449,48 @@ pub struct Localnet {
435449
started_at: SystemTime,
436450
}
437451

452+
#[derive(Default)]
453+
struct RecoveryPoints {
454+
next_id: u64,
455+
points: Vec<RecoveryPoint>,
456+
}
457+
458+
struct RecoveryPoint {
459+
id: u64,
460+
snapshot: NodeStateSnapshot,
461+
}
462+
463+
impl RecoveryPoints {
464+
fn create(&mut self, node: &Node) -> anyhow::Result<LocalnetRecoveryPointResult> {
465+
let snapshot = node.build_snapshot()?;
466+
self.next_id = self
467+
.next_id
468+
.checked_add(1)
469+
.context("Recovery point id overflow")?;
470+
let id = self.next_id;
471+
let block_seqno = snapshot.globals.head_seqno;
472+
self.points.push(RecoveryPoint { id, snapshot });
473+
Ok(LocalnetRecoveryPointResult { id, block_seqno })
474+
}
475+
476+
fn revert(&mut self, node: &mut Node, id: u64) -> anyhow::Result<LocalnetRecoveryPointResult> {
477+
let index = self
478+
.points
479+
.iter()
480+
.position(|point| point.id == id)
481+
.with_context(|| format!("Recovery point {id} not found"))?;
482+
let snapshot = self.points[index].snapshot.clone();
483+
let block_seqno = snapshot.globals.head_seqno;
484+
node.apply_snapshot(snapshot)?;
485+
self.points.truncate(index);
486+
Ok(LocalnetRecoveryPointResult { id, block_seqno })
487+
}
488+
489+
fn clear(&mut self) {
490+
self.points.clear();
491+
}
492+
}
493+
438494
pub const DEFAULT_BLOCK_INTERVAL_MS: u64 = 500;
439495

440496
impl Localnet {
@@ -1043,6 +1099,23 @@ impl Localnet {
10431099
rx.await?
10441100
}
10451101

1102+
pub async fn create_recovery_point(&self) -> anyhow::Result<LocalnetRecoveryPointResult> {
1103+
let (resp, rx) = oneshot::channel();
1104+
self.tx.send(Request::CreateRecoveryPoint { resp }).await?;
1105+
rx.await?
1106+
}
1107+
1108+
pub async fn revert_recovery_point(
1109+
&self,
1110+
id: u64,
1111+
) -> anyhow::Result<LocalnetRecoveryPointResult> {
1112+
let (resp, rx) = oneshot::channel();
1113+
self.tx
1114+
.send(Request::RevertRecoveryPoint { id, resp })
1115+
.await?;
1116+
rx.await?
1117+
}
1118+
10461119
pub async fn mine_blocks(&self, count: u32) -> anyhow::Result<LocalnetMineResult> {
10471120
let (resp, rx) = oneshot::channel();
10481121
self.tx.send(Request::MineBlocks { count, resp }).await?;
@@ -1097,6 +1170,7 @@ fn run_node_loop(
10971170
auto_mining: bool,
10981171
) -> anyhow::Result<()> {
10991172
let mut node = create_node(events_tx, state_source, db_path)?;
1173+
let mut recovery_points = RecoveryPoints::default();
11001174
tracing::info!(
11011175
"TON localnet started, block interval: {}ms, auto mining: {}",
11021176
block_interval.as_millis(),
@@ -1105,7 +1179,7 @@ fn run_node_loop(
11051179

11061180
if !auto_mining {
11071181
while let Some(req) = rx.blocking_recv() {
1108-
process_loop_request(&mut node, req);
1182+
process_loop_request(&mut node, &mut recovery_points, req);
11091183
}
11101184
return Ok(());
11111185
}
@@ -1138,6 +1212,7 @@ async fn run_node_loop_async(
11381212
block_interval: Duration,
11391213
) -> anyhow::Result<()> {
11401214
let mut next_block_at = Instant::now() + block_interval;
1215+
let mut recovery_points = RecoveryPoints::default();
11411216

11421217
loop {
11431218
if Instant::now() >= next_block_at {
@@ -1154,7 +1229,7 @@ async fn run_node_loop_async(
11541229
let Some(req) = req else {
11551230
return Ok(());
11561231
};
1157-
process_loop_request(&mut node, req);
1232+
process_loop_request(&mut node, &mut recovery_points, req);
11581233
}
11591234
}
11601235
}
@@ -1188,7 +1263,7 @@ fn handle_mine_blocks(node: &mut Node, count: u32) -> anyhow::Result<LocalnetMin
11881263
})
11891264
}
11901265

1191-
fn process_loop_request(node: &mut Node, req: Request) {
1266+
fn process_loop_request(node: &mut Node, recovery_points: &mut RecoveryPoints, req: Request) {
11921267
tracing::debug!("Node loop processing request: {:?}", req);
11931268
match req {
11941269
Request::SendBoc { boc, resp } => {
@@ -1456,6 +1531,17 @@ fn process_loop_request(node: &mut Node, req: Request) {
14561531
}
14571532
Request::LoadState { path, resp } => {
14581533
let res = node.load_state_from_path(path);
1534+
if res.is_ok() {
1535+
recovery_points.clear();
1536+
}
1537+
let _ = resp.send(res);
1538+
}
1539+
Request::CreateRecoveryPoint { resp } => {
1540+
let res = recovery_points.create(node);
1541+
let _ = resp.send(res);
1542+
}
1543+
Request::RevertRecoveryPoint { id, resp } => {
1544+
let res = recovery_points.revert(node, id);
14591545
let _ = resp.send(res);
14601546
}
14611547
Request::MineBlocks { count, resp } => {

0 commit comments

Comments
 (0)