From 780034801ff371cc0459cbfbade1092251eced52 Mon Sep 17 00:00:00 2001 From: Kisaragi Marine Date: Tue, 26 Mar 2024 22:57:30 +0900 Subject: [PATCH] =?UTF-8?q?fix(persistence):=20=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=92=E6=9B=B4=E6=96=B0=E3=81=99=E3=82=8B=E6=99=82?= =?UTF-8?q?=E3=80=81=E5=B8=B8=E3=81=AB=E5=85=88=E9=A0=AD=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E3=81=B8=E5=B7=BB=E3=81=8D=E6=88=BB=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 40 ++++++--- packages/toy-blog/Cargo.toml | 5 ++ packages/toy-blog/src/service/persistence.rs | 87 +++++++++++++++++--- 3 files changed, 109 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70582e4..7b58fc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -601,14 +601,20 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] +[[package]] +name = "fastrand" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" + [[package]] name = "fern" version = "0.6.2" @@ -875,15 +881,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.149" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "linux-raw-sys" -version = "0.4.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "local-channel" @@ -1160,15 +1166,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.20" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1343,6 +1349,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "thiserror" version = "1.0.58" @@ -1444,6 +1462,7 @@ name = "toy-blog" version = "0.5.3" dependencies = [ "actix-cors", + "actix-service", "actix-web", "actix-web-httpauth", "anyhow", @@ -1457,6 +1476,7 @@ dependencies = [ "serde", "serde_json", "strum", + "tempfile", "thiserror", "tokio", "toy-blog-endpoint-model", diff --git a/packages/toy-blog/Cargo.toml b/packages/toy-blog/Cargo.toml index 9807f6c..89a8cfa 100644 --- a/packages/toy-blog/Cargo.toml +++ b/packages/toy-blog/Cargo.toml @@ -8,6 +8,7 @@ metadata = { cargo-udeps = { ignore = [["thiserror"]] } } [dependencies] actix-cors = "0.7.0" +actix-service = "2.0.2" actix-web = "4.5.1" actix-web-httpauth = "0.8.1" anyhow = "1.0.81" @@ -27,3 +28,7 @@ toy-blog-endpoint-model = { path = "../toy-blog-endpoint-model" } [features] unstable_activitypub = [] + +[dev-dependencies] +tempfile = "3.10.1" +tokio = { version = "1.36.0", features = ["macros", "rt"] } diff --git a/packages/toy-blog/src/service/persistence.rs b/packages/toy-blog/src/service/persistence.rs index 6b700da..bd6090c 100644 --- a/packages/toy-blog/src/service/persistence.rs +++ b/packages/toy-blog/src/service/persistence.rs @@ -15,25 +15,30 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use toy_blog_endpoint_model::{ArticleId, FlatId, ListArticleResponse, Visibility}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ArticleRepository { cache: Arc>, - invalidated: AtomicBool, + invalidated: Arc, file_lock: Arc>, } impl ArticleRepository { - fn create_default_file_if_absent(path: impl AsRef) { + // TODO: visible for test + pub(super) fn create_default_file_if_absent(path: impl AsRef) { if !path.as_ref().exists() { - info!("creating article table"); - let mut file = File::options().write(true).read(true).create(true).truncate(false).open(path.as_ref()).unwrap(); - write!( - &mut (file), - "{default_json}", - default_json = serde_json::to_string(&FileScheme::empty()).unwrap() - ).unwrap(); + Self::init(path) } } + + pub(super) fn init(path: impl AsRef) { + info!("creating article table"); + let mut file = File::options().write(true).read(true).create(true).truncate(false).open(path.as_ref()).unwrap(); + write!( + &mut (file), + "{default_json}", + default_json = serde_json::to_string(&FileScheme::empty()).unwrap() + ).unwrap(); + } pub async fn new(path: impl AsRef) -> Self { Self::create_default_file_if_absent(path.as_ref()); @@ -45,7 +50,7 @@ impl ArticleRepository { Self { cache: Arc::new(RwLock::new(Self::parse_file_as_json_static(&mut lock).expect("crash"))), - invalidated: AtomicBool::new(false), + invalidated: Arc::new(AtomicBool::new(false)), file_lock: Arc::new(RwLock::new(lock)), } } @@ -63,11 +68,13 @@ impl ArticleRepository { } fn save(&self) -> Result<(), serde_json::Error> { + let r = &mut **self.file_lock.write().expect("file lock is poisoned"); + r.rewind().expect("seek"); serde_json::to_writer( - &mut **self.file_lock.write().expect("file lock is poisoned"), + r, &&*self.cache.read().expect("cache is poisoned") )?; - trace!("saved"); + debug!("saved"); Ok(()) } @@ -288,3 +295,57 @@ enum FileLockError { #[error("timeout")] Timeout, } + +#[cfg(test)] +mod tests { + use fern::colors::ColoredLevelConfig; + use toy_blog_endpoint_model::{ArticleId, Visibility}; + use crate::service::persistence::ArticleRepository; + + fn setup_logger() -> anyhow::Result<()> { + let colors = ColoredLevelConfig::new(); + fern::Dispatch::new() + .format(move |out, message, record| { + out.finish(format_args!( + "{}[{}][{}] {}", + chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), + record.target(), + colors.color(record.level()), + message + )); + }) + .level(log::LevelFilter::Debug) + .chain(std::io::stdout()) + // .chain(fern::log_file("output.log")?) + .apply()?; + Ok(()) + } + + #[test] + fn check_file_cursor_position_is_rewinded_to_its_start() { + /* 想定シナリオ + * 1. 新しく作成 + * 2. 何らかの記事を作る + * 3. repoのセッションを閉じる + * 4. もう一度開こうとした時にエラーにならないこと + */ + setup_logger().expect("log"); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + let m = tempfile::NamedTempFile::new().expect("failed to initialize temporary file"); + ArticleRepository::init(m.path()); + let temp_repo = ArticleRepository::new(m.path()).await; + temp_repo.create_entry(&ArticleId::new("12345".to_string()), "12345 Hello".to_string(), Visibility::Private).await.expect("failed to save"); + temp_repo.create_entry(&ArticleId::new("23456".to_string()), "23456 Hello".to_string(), Visibility::Private).await.expect("failed to save"); + drop(temp_repo); + let temp_repo = ArticleRepository::new(m.path()).await; + let y = temp_repo.entries().iter().find(|x| x.0 == ArticleId::new("12345".to_string())).expect("12345").1.content == "12345 Hello"; + assert!(y); + let y = temp_repo.entries().iter().find(|x| x.0 == ArticleId::new("23456".to_string())).expect("23456").1.content == "23456 Hello"; + assert!(y); + }); + } +}