事务模型
This commit is contained in:
1 parent
274b7fcdcc
commit
045ccc2dc0
20 files changed
+1519
-294
No files matched your search
@@ -216,6 +216,67 @@ fn concurrent_shard_reads_exclude_writers() -> Result<(), Box<dyn std::error::Er
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 跨库并发互不串数据,目录查询不被写饿死
|
||||
#[test]
|
||||
fn concurrent_requests_across_databases_stay_isolated() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (_srv, _data) = start_server();
|
||||
let mut c = connect()?;
|
||||
let request = |c: &mut Conn, value: serde_json::Value| -> Result<serde_json::Value, serde_json::Error> {
|
||||
serde_json::from_str(&send(c, &value.to_string()))
|
||||
};
|
||||
let databases = ["alpha", "beta"];
|
||||
for database in databases {
|
||||
assert_eq!(request(&mut c, serde_json::json!({"method":"create_database","database":database}))?["status"], "ok");
|
||||
assert_eq!(
|
||||
request(&mut c, serde_json::json!({"method":"create_table","database":database,"table":"items","columns":[{"name":"id","ty":"int"}]}))?["status"],
|
||||
"ok"
|
||||
);
|
||||
}
|
||||
|
||||
let storage = CURRENT.with(|slot| slot.borrow().as_ref().cloned().expect("storage opened"));
|
||||
let written = 40;
|
||||
std::thread::scope(|scope| {
|
||||
let writers: Vec<_> = databases
|
||||
.iter()
|
||||
.map(|database| {
|
||||
let storage = Arc::clone(&storage);
|
||||
let database = database.to_string();
|
||||
scope.spawn(move || {
|
||||
for i in 1..=written {
|
||||
let line = format!(
|
||||
r#"{{"method":"insert","database":"{database}","table":"items","row":{{"id":{i}}}}}"#
|
||||
);
|
||||
let value: serde_json::Value = serde_json::from_str(&storage.request_line(&line)).expect("json");
|
||||
assert_eq!(value["status"], "ok", "{database}: {value}");
|
||||
}
|
||||
database
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let catalog = {
|
||||
let storage = Arc::clone(&storage);
|
||||
scope.spawn(move || {
|
||||
for _ in 0..40 {
|
||||
for line in [r#"{"method":"list_databases"}"#, r#"{"method":"all_catalogs"}"#] {
|
||||
let value: serde_json::Value = serde_json::from_str(&storage.request_line(line)).expect("json");
|
||||
assert_ne!(value["status"], "error", "{line}: {value}");
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
for writer in writers {
|
||||
writer.join().expect("writer thread");
|
||||
}
|
||||
catalog.join().expect("catalog thread");
|
||||
});
|
||||
|
||||
for database in databases {
|
||||
let rows = request(&mut c, serde_json::json!({"method":"scan","database":database,"table":"items"}))?["rows"].clone();
|
||||
assert_eq!(rows.as_array().expect("rows").len(), written, "{database} 行数不对");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 删列不改堆字节,重启后不再暴露该列
|
||||
#[test]
|
||||
fn drop_column_keeps_heap_bytes_and_hides_values_after_restart() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -744,6 +805,12 @@ fn account_failure_preserves_wal_and_blocks_writes() -> Result<(), Box<dyn std::
|
||||
let mut c = connect()?;
|
||||
let recovered = send(&mut c, r#"{"method":"accounts_list"}"#);
|
||||
assert!(recovered.contains("hash-one"), "{recovered}");
|
||||
let status: serde_json::Value =
|
||||
serde_json::from_str(&send(&mut c, r#"{"method":"system_status"}"#))?;
|
||||
assert!(
|
||||
status["last_lsn"].as_u64().unwrap_or(0) >= 1,
|
||||
"重放后检查点要推进: {status}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1015,6 +1082,125 @@ fn delete_keys_removes_row_and_index_entry() {
|
||||
assert!(r.contains(r#""status":"ok""#), "id 2 should be insertable again: {}", r);
|
||||
}
|
||||
|
||||
/// 事务提交:一批写操作在同一个请求里落地
|
||||
#[test]
|
||||
fn apply_transaction_lands_upserts_and_deletes_together() {
|
||||
let (_srv, _data) = start_server();
|
||||
let mut c = connect().unwrap();
|
||||
|
||||
for (id, name) in [(1, "Alice"), (2, "Bob")] {
|
||||
let req = format!(
|
||||
r#"{{"method":"insert","table":"txn_t","row":{{"id":{},"name":"{}"}}}}"#,
|
||||
id, name
|
||||
);
|
||||
send(&mut c, &req);
|
||||
}
|
||||
|
||||
let batch = r#"{"method":"apply_transaction","ops":[
|
||||
{"op":"delete","table":"txn_t","ids":[1]},
|
||||
{"op":"upsert","table":"txn_t","rows":[{"id":3,"name":"Carol"}]}
|
||||
]}"#;
|
||||
let r = send(&mut c, batch);
|
||||
assert!(r.contains(r#""status":"ok""#), "batch: {}", r);
|
||||
|
||||
let r = send(&mut c, r#"{"method":"scan","table":"txn_t"}"#);
|
||||
assert!(!r.contains("Alice"), "deleted row should be gone: {}", r);
|
||||
assert!(r.contains("Bob") && r.contains("Carol"), "kept and added rows: {}", r);
|
||||
|
||||
for line in [
|
||||
r#"{"method":"apply_transaction","ops":[{"op":"upsert","table":"__system_users","rows":[{"id":1}]}]}"#,
|
||||
r#"{"method":"apply_transaction","ops":[{"op":"delete","table":"../evil","ids":[1]}]}"#,
|
||||
] {
|
||||
let r = send(&mut c, line);
|
||||
assert!(r.contains(r#""status":"error""#), "bad op rejected: {} -> {}", line, r);
|
||||
}
|
||||
|
||||
// 没有整数 id 的表用整表替换提交
|
||||
let r = send(&mut c, r#"{"method":"insert","table":"txn_no_id","row":{"name":"old"}}"#);
|
||||
assert!(r.contains(r#""status":"ok""#), "insert: {}", r);
|
||||
let replace = r#"{"method":"apply_transaction","ops":[{"op":"replace","table":"txn_no_id","rows":[{"name":"only"}]}]}"#;
|
||||
let r = send(&mut c, replace);
|
||||
assert!(r.contains(r#""status":"ok""#), "replace: {}", r);
|
||||
let r = send(&mut c, r#"{"method":"scan","table":"txn_no_id"}"#);
|
||||
assert!(r.contains("only") && !r.contains("old"), "replaced rows: {}", r);
|
||||
}
|
||||
|
||||
/// 标记已落盘、数据未改的崩溃现场:重启重放并推进检查点
|
||||
#[test]
|
||||
fn committed_group_replays_after_restart() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use chusql_core_storage::wal::{Wal, WalOp};
|
||||
let (server, data) = start_server();
|
||||
let mut c = connect()?;
|
||||
request_ok(&mut c, serde_json::json!({"method":"insert","table":"items","row":{"id":1,"secret":"old"}}))?;
|
||||
drop(c);
|
||||
drop(server);
|
||||
|
||||
let mut added = std::collections::HashMap::new();
|
||||
added.insert("id".to_string(), serde_json::json!(2));
|
||||
added.insert("secret".to_string(), serde_json::json!("new"));
|
||||
let group = vec![
|
||||
WalOp::InsertBatch {
|
||||
table: "items".into(),
|
||||
rows: vec![added.clone()],
|
||||
},
|
||||
WalOp::DeleteKeys {
|
||||
table: "items".into(),
|
||||
keys: vec![1],
|
||||
},
|
||||
];
|
||||
let wal = Wal::new(data.path().join("databases/main/wal.log"));
|
||||
let commit_lsn = wal.append_group(&group)?;
|
||||
drop(wal);
|
||||
|
||||
let _server = start_server_in(data.path());
|
||||
let mut c = connect()?;
|
||||
let rows: serde_json::Value =
|
||||
serde_json::from_str(&send(&mut c, r#"{"method":"scan","table":"items"}"#))?;
|
||||
assert_eq!(rows["rows"], serde_json::json!([{"id":2,"secret":"new"}]));
|
||||
assert_eq!(std::fs::metadata(data.path().join("databases/main/wal.log"))?.len(), 0);
|
||||
let checkpoint = std::fs::read(data.path().join("databases/main/wal.checkpoint"))?;
|
||||
assert_eq!(checkpoint.len(), 8, "检查点要落盘");
|
||||
assert_eq!(
|
||||
u64::from_le_bytes(checkpoint[..8].try_into()?),
|
||||
commit_lsn,
|
||||
"检查点记到提交 LSN"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 没有提交标记的记录组在重启时整组丢掉
|
||||
#[test]
|
||||
fn uncommitted_group_is_discarded_on_restart() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use chusql_core_storage::wal::{Wal, WalOp};
|
||||
let (server, data) = start_server();
|
||||
let mut c = connect()?;
|
||||
request_ok(&mut c, serde_json::json!({"method":"insert","table":"items","row":{"id":1,"secret":"old"}}))?;
|
||||
drop(c);
|
||||
drop(server);
|
||||
|
||||
let mut lost = std::collections::HashMap::new();
|
||||
lost.insert("id".to_string(), serde_json::json!(2));
|
||||
lost.insert("secret".to_string(), serde_json::json!("lost"));
|
||||
let wal = Wal::new(data.path().join("databases/main/wal.log"));
|
||||
wal.append(&WalOp::InsertBatch {
|
||||
table: "items".into(),
|
||||
rows: vec![lost.clone()],
|
||||
})?;
|
||||
wal.append(&WalOp::InsertBatch {
|
||||
table: "items".into(),
|
||||
rows: vec![lost],
|
||||
})?;
|
||||
drop(wal);
|
||||
|
||||
let _server = start_server_in(data.path());
|
||||
let mut c = connect()?;
|
||||
let rows: serde_json::Value =
|
||||
serde_json::from_str(&send(&mut c, r#"{"method":"scan","table":"items"}"#))?;
|
||||
assert_eq!(rows["rows"], serde_json::json!([{"id":1,"secret":"old"}]));
|
||||
assert_eq!(std::fs::metadata(data.path().join("databases/main/wal.log"))?.len(), 0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 给第二列建索引后能按那一列查
|
||||
#[test]
|
||||
fn create_index_on_secondary_column() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use chusql_core_storage::protocol::Row;
|
||||
use chusql_core_storage::wal::{Wal, WalOp};
|
||||
use chusql_core_storage::wal::{committed_ops, Wal, WalOp};
|
||||
use serde_json::json;
|
||||
|
||||
// WAL 测试:追加、读取、清空、截断尾巴。
|
||||
// WAL 测试:追加、读取、清空、截断尾巴、LSN 与提交标记。
|
||||
|
||||
|
||||
/// 用键值对构造一行
|
||||
@@ -99,3 +99,122 @@ fn truncated_tail_is_ignored() {
|
||||
assert!(wal.read().unwrap().is_none());
|
||||
}
|
||||
|
||||
/// 每条记录带递增的 LSN,重开也从文件里的最大号往下走
|
||||
#[test]
|
||||
fn every_record_gets_an_increasing_lsn() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("wal.log");
|
||||
let wal = Wal::new(&path);
|
||||
|
||||
for id in 1..=3 {
|
||||
wal.append(&WalOp::Insert {
|
||||
table: "t".into(),
|
||||
row: row(&[("id", json!(id))]),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
let scan = wal.read_all().unwrap();
|
||||
let lsns: Vec<u64> = scan.records.iter().map(|(lsn, _)| *lsn).collect();
|
||||
assert_eq!(lsns, vec![1, 2, 3]);
|
||||
assert_eq!(scan.good_len as u64, std::fs::metadata(&path).unwrap().len());
|
||||
|
||||
let reopened = Wal::new(&path);
|
||||
reopened
|
||||
.append(&WalOp::Insert {
|
||||
table: "t".into(),
|
||||
row: row(&[("id", json!(4))]),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(reopened.read_all().unwrap().records.last().unwrap().0, 4);
|
||||
}
|
||||
|
||||
/// 一组操作写完带提交标记,标记的 LSN 也在记录里
|
||||
#[test]
|
||||
fn append_group_writes_a_commit_marker() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let wal = Wal::new(dir.path().join("wal.log"));
|
||||
|
||||
let group = [
|
||||
WalOp::InsertBatch {
|
||||
table: "t".into(),
|
||||
rows: vec![row(&[("id", json!(2))])],
|
||||
},
|
||||
WalOp::DeleteKeys {
|
||||
table: "t".into(),
|
||||
keys: vec![1],
|
||||
},
|
||||
];
|
||||
let commit_lsn = wal.append_group(&group).unwrap();
|
||||
|
||||
let scan = wal.read_all().unwrap();
|
||||
assert_eq!(scan.records.len(), 3);
|
||||
assert_eq!(scan.records.last().unwrap().1, WalOp::Commit);
|
||||
assert_eq!(scan.records.last().unwrap().0, commit_lsn);
|
||||
assert_eq!(wal.read().unwrap().unwrap(), group[0]);
|
||||
assert_eq!(
|
||||
committed_ops(&scan.records),
|
||||
vec![(1, group[0].clone()), (2, group[1].clone())]
|
||||
);
|
||||
}
|
||||
|
||||
/// 没有提交标记的记录组算未提交,旧格式的单条仍算已提交
|
||||
#[test]
|
||||
fn records_without_a_commit_marker_stay_uncommitted() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let wal = Wal::new(dir.path().join("wal.log"));
|
||||
|
||||
for id in 1..=2 {
|
||||
wal.append(&WalOp::Insert {
|
||||
table: "t".into(),
|
||||
row: row(&[("id", json!(id))]),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
assert!(committed_ops(&wal.read_all().unwrap().records).is_empty());
|
||||
|
||||
wal.clear().unwrap();
|
||||
wal.append(&WalOp::Insert {
|
||||
table: "t".into(),
|
||||
row: row(&[("id", json!(9))]),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(committed_ops(&wal.read_all().unwrap().records).len(), 1);
|
||||
}
|
||||
|
||||
/// 撕成两半的尾巴能读出完好长度并截掉
|
||||
#[test]
|
||||
fn trailing_torn_frame_can_be_truncated() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("wal.log");
|
||||
let wal = Wal::new(&path);
|
||||
|
||||
for id in 1..=2 {
|
||||
wal.append(&WalOp::Insert {
|
||||
table: "t".into(),
|
||||
row: row(&[("id", json!(id))]),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
let full = std::fs::read(&path).unwrap();
|
||||
std::fs::write(&path, &full[..full.len() - 3]).unwrap();
|
||||
|
||||
let scan = wal.read_all().unwrap();
|
||||
assert_eq!(scan.records.len(), 1);
|
||||
assert!(scan.good_len < full.len() - 3);
|
||||
|
||||
wal.truncate_to(scan.good_len).unwrap();
|
||||
assert_eq!(std::fs::metadata(&path).unwrap().len(), scan.good_len as u64);
|
||||
assert_eq!(wal.read_all().unwrap().records.len(), 1);
|
||||
}
|
||||
|
||||
/// 检查点写读一致
|
||||
#[test]
|
||||
fn checkpoint_round_trips() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let wal = Wal::new(dir.path().join("wal.log"));
|
||||
|
||||
assert_eq!(wal.read_checkpoint().unwrap(), None);
|
||||
wal.write_checkpoint(7).unwrap();
|
||||
assert_eq!(wal.read_checkpoint().unwrap(), Some(7));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user