Skip to content

Commit 6b41e7c

Browse files
weiGina
authored andcommitted
fix(zk): checkpoint reconciliation after staged entries
1 parent a60f822 commit 6b41e7c

2 files changed

Lines changed: 79 additions & 4 deletions

File tree

dt-connector/src/extractor/zk/zk_extractor.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,21 +190,31 @@ impl ZkExtractor {
190190
scan_result.entries.push(entry);
191191
}
192192

193-
let position = self.build_position(&scan_result.versions, scan_result.high_water_zxid);
193+
let final_position =
194+
self.build_position(&scan_result.versions, scan_result.high_water_zxid);
195+
let emitted_entries = scan_result.entries.len();
194196
for entry in scan_result.entries {
195197
self.extract_state
196-
.push_dt_data(&self.base_extractor, DtData::Zk { entry }, position.clone())
198+
.push_dt_data(
199+
&self.base_extractor,
200+
DtData::Zk { entry },
201+
Self::staged_entry_position(),
202+
)
197203
.await?;
198204
}
205+
self.extract_state
206+
.push_dt_data(&self.base_extractor, DtData::Heartbeat {}, final_position)
207+
.await?;
199208

200209
*path_versions = scan_result.versions;
201210
*high_water_zxid = scan_result.high_water_zxid;
202211

203212
log_info!(
204-
"ZkExtractor reconciliation complete: reason={}, {} paths, high_water_zxid={}",
213+
"ZkExtractor reconciliation complete: reason={}, {} paths, high_water_zxid={}, emitted_entries={}",
205214
reason,
206215
path_versions.len(),
207-
high_water_zxid
216+
high_water_zxid,
217+
emitted_entries
208218
);
209219
Ok(())
210220
}
@@ -235,6 +245,10 @@ impl ZkExtractor {
235245
root == "/" || path == root || path.starts_with(&format!("{}/", root.trim_end_matches('/')))
236246
}
237247

248+
fn staged_entry_position() -> Position {
249+
Position::None
250+
}
251+
238252
async fn full_scan(
239253
&mut self,
240254
client: &ZkClient,
@@ -688,4 +702,9 @@ mod tests {
688702
assert!(scan_result.versions.is_empty());
689703
assert!(scan_result.entries.is_empty());
690704
}
705+
706+
#[test]
707+
fn staged_reconciliation_entries_do_not_carry_final_position() {
708+
assert_eq!(ZkExtractor::staged_entry_position(), Position::None);
709+
}
691710
}

dt-pipeline/src/base_pipeline.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,7 @@ mod tests {
663663
dt_data::{DtData, DtItem},
664664
position::Position,
665665
redis::redis_entry::RedisEntry,
666+
zk::{zk_entry::ZkEntry, zk_event_type::ZkEventType, zk_stat::ZkStat},
666667
};
667668
use dt_connector::extractor::resumer::utils::ResumerUtil;
668669

@@ -690,6 +691,34 @@ mod tests {
690691
}
691692
}
692693

694+
fn zk_item(position: Position) -> DtItem {
695+
DtItem {
696+
dt_data: DtData::Zk {
697+
entry: ZkEntry {
698+
path: "/app/service".to_string(),
699+
data: Some(b"value".to_vec()),
700+
stat: ZkStat::default(),
701+
ephemeral: false,
702+
event_type: ZkEventType::Updated,
703+
source_id: "node-a".to_string(),
704+
source_order_millis: 1,
705+
source_zxid: 1,
706+
order_origin: Default::default(),
707+
},
708+
},
709+
position,
710+
data_origin_node: String::new(),
711+
}
712+
}
713+
714+
fn heartbeat_item(position: Position) -> DtItem {
715+
DtItem {
716+
dt_data: DtData::Heartbeat {},
717+
position,
718+
data_origin_node: String::new(),
719+
}
720+
}
721+
693722
#[test]
694723
fn fetch_raw_collects_latest_position_per_redis_node() {
695724
let mut pending_snapshot_finished = HashMap::new();
@@ -713,4 +742,31 @@ mod tests {
713742
assert_eq!(by_key.get("redis-node-node-1"), Some(&node_1_new));
714743
assert_eq!(by_key.get("redis-node-node-2"), Some(&node_2));
715744
}
745+
746+
#[test]
747+
fn fetch_raw_advances_zk_position_only_on_trailing_heartbeat() {
748+
let mut pending_snapshot_finished = HashMap::new();
749+
let final_position = Position::Zk {
750+
path_versions: HashMap::from([("/app/service".to_string(), (1, 10))]),
751+
last_scan_timestamp: 100,
752+
high_water_zxid: 10,
753+
total_paths: 1,
754+
};
755+
let partial_data = vec![zk_item(Position::None)];
756+
757+
let (_, partial_position, partial_commits) =
758+
BasePipeline::fetch_raw(&partial_data, &mut pending_snapshot_finished);
759+
assert_eq!(partial_position, Some(Position::None));
760+
assert!(partial_commits.is_empty());
761+
762+
let complete_data = vec![
763+
zk_item(Position::None),
764+
heartbeat_item(final_position.clone()),
765+
];
766+
let (_, complete_position, complete_commits) =
767+
BasePipeline::fetch_raw(&complete_data, &mut pending_snapshot_finished);
768+
769+
assert_eq!(complete_position, Some(final_position.clone()));
770+
assert_eq!(complete_commits, vec![final_position]);
771+
}
716772
}

0 commit comments

Comments
 (0)