radicle: implement caching for issues and patches

Implement caching for the `Issue` and `Patch` COB. This is achieved by
having the `Transaction::initial` and `Transaction::commit` methods
take a cache which can update the newly created/updated object. The
`Store::remove` method also takes the cache for removing the object
from the cache, as well as the repository.

This meant that the `*Mut` types for the respective COBs are required
to carry a cache alongside the repository store. Identities will not
be cached, and so this is always set to `NoCache` -- which performs no
caching, and always succeeds.

To perform cache reading for issues and patches, the `cache::Issues`
and `cache::Patches` traits are introduced. They capture the minimal
amount of methods that are needed for the `rad issue` and `rad patch`
CLI commands.

The implementor for each of these traits is there respective `Cache`
types, which combines their backing repository store and the SQLite
database. They both provide convenience methods:
- `create` (and `draft` for patches)
- `get_mut`
- `remove`
which can be used instead of their repository store counterparts.

All uses of the repository stores (where needed) are replaced with
`Cache` types.

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
Fintan Halpenny
2024-02-23 16:03:21 +01:00
committed by cloudhead
parent 67e46952b6
commit 02fb9397e8
5 changed files with 44 additions and 28 deletions
+4 -2
View File
@@ -9,6 +9,8 @@ use axum::http::Method;
use axum::response::{IntoResponse, Json};
use axum::routing::get;
use axum::Router;
use radicle::issue::cache::Issues as _;
use radicle::patch::cache::Patches as _;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tokio::sync::RwLock;
@@ -60,8 +62,8 @@ impl Context {
let payload = doc.project()?;
let delegates = doc.delegates;
let issues = issue::Issues::open(&repo)?.counts()?;
let patches = patch::Patches::open(&repo)?.counts()?;
let issues = self.profile.issues(&repo)?.counts()?;
let patches = self.profile.patches(&repo)?.counts()?;
let db = &self.profile.database()?;
let seeding = db.count(&id).unwrap_or_default();
+13
View File
@@ -30,6 +30,14 @@ pub enum Error {
#[error(transparent)]
Storage(#[from] radicle::storage::Error),
/// Cob cache error.
#[error(transparent)]
CobCache(#[from] radicle::cob::cache::Error),
/// Cob issue cache error.
#[error(transparent)]
CacheIssue(#[from] radicle::cob::issue::cache::Error),
/// Cob issue error.
#[error(transparent)]
CobIssue(#[from] radicle::cob::issue::Error),
@@ -38,6 +46,10 @@ pub enum Error {
#[error(transparent)]
CobPatch(#[from] radicle::cob::patch::Error),
/// Cob patch cache error.
#[error(transparent)]
CachePatch(#[from] radicle::cob::patch::cache::Error),
/// Cob store error.
#[error(transparent)]
CobStore(#[from] radicle::cob::store::Error),
@@ -123,6 +135,7 @@ impl IntoResponse for Error {
Error::BadRequest(msg) => (StatusCode::BAD_REQUEST, Some(msg)),
other => {
tracing::error!("Error: {message}");
tracing::debug!("Error Debug: {:?}", other);
if cfg!(debug_assertions) {
(StatusCode::INTERNAL_SERVER_ERROR, Some(other.to_string()))
+4 -5
View File
@@ -5,10 +5,10 @@ use axum::response::IntoResponse;
use axum::routing::get;
use axum::{Json, Router};
use radicle::cob::issue::Issues;
use radicle::cob::patch::Patches;
use radicle::identity::{Did, Visibility};
use radicle::issue::cache::Issues as _;
use radicle::node::routing::Store;
use radicle::patch::cache::Patches as _;
use radicle::storage::{ReadRepository, ReadStorage};
use crate::api::error::Error;
@@ -44,7 +44,6 @@ async fn delegates_projects_handler(
let storage = &ctx.profile.storage;
let db = &ctx.profile.database()?;
let pinned = &ctx.profile.config.web.pinned;
let mut projects = match show {
ProjectQuery::All => storage
.repositories()?
@@ -73,13 +72,13 @@ async fn delegates_projects_handler(
let Ok(payload) = id.doc.project() else {
return None;
};
let Ok(issues) = Issues::open(&repo) else {
let Ok(issues) = ctx.profile.issues(&repo) else {
return None;
};
let Ok(issues) = issues.counts() else {
return None;
};
let Ok(patches) = Patches::open(&repo) else {
let Ok(patches) = ctx.profile.patches(&repo) else {
return None;
};
let Ok(patches) = patches.counts() else {
+19 -15
View File
@@ -14,7 +14,10 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use tower_http::set_header::SetResponseHeaderLayer;
use radicle::cob::{issue, patch, resolve_embed, Embed, Label, Uri};
use radicle::cob::{
issue, issue::cache::Issues as _, patch, patch::cache::Patches as _, resolve_embed, Embed,
Label, Uri,
};
use radicle::identity::{Did, RepoId, Visibility};
use radicle::node::routing::Store;
use radicle::node::{AliasStore, Node, NodeId};
@@ -126,13 +129,13 @@ async fn project_root_handler(
let Ok(payload) = info.doc.project() else {
return None;
};
let Ok(issues) = issue::Issues::open(&repo) else {
let Ok(issues) = ctx.profile.issues(&repo) else {
return None;
};
let Ok(issues) = issues.counts() else {
return None;
};
let Ok(patches) = patch::Patches::open(&repo) else {
let Ok(patches) = ctx.profile.patches(&repo) else {
return None;
};
let Ok(patches) = patches.counts() else {
@@ -587,9 +590,9 @@ async fn issues_handler(
let state = state.unwrap_or_default();
let storage = &ctx.profile.storage;
let repo = storage.repository(project)?;
let issues = issue::Issues::open(&repo)?;
let issues = ctx.profile.issues(&repo)?;
let mut issues: Vec<_> = issues
.all()?
.list()?
.filter_map(|r| {
let (id, issue) = r.ok()?;
(state.matches(issue.state())).then_some((id, issue))
@@ -639,7 +642,7 @@ async fn issue_create_handler(
.filter_map(|embed| resolve_embed(&repo, embed))
.collect();
let mut issues = issue::Issues::open(&repo)?;
let mut issues = ctx.profile.issues_mut(&repo)?;
let issue = issues
.create(
issue.title,
@@ -672,7 +675,7 @@ async fn issue_update_handler(
let storage = &ctx.profile.storage;
let signer = ctx.profile.signer()?;
let repo = storage.repository(project)?;
let mut issues = issue::Issues::open(&repo)?;
let mut issues = ctx.profile.issues_mut(&repo)?;
let mut issue = issues.get_mut(&issue_id.into())?;
let id = match action {
@@ -723,7 +726,9 @@ async fn issue_handler(
) -> impl IntoResponse {
let storage = &ctx.profile.storage;
let repo = storage.repository(project)?;
let issue = issue::Issues::open(&repo)?
let issue = ctx
.profile
.issues(&repo)?
.get(&issue_id.into())?
.ok_or(Error::NotFound)?;
let aliases = ctx.profile.aliases();
@@ -756,7 +761,7 @@ async fn patch_create_handler(
.signer()
.map_err(|_| Error::Auth("Unauthorized"))?;
let repo = storage.repository(project)?;
let mut patches = patch::Patches::open(&repo)?;
let mut patches = ctx.profile.patches_mut(&repo)?;
let base_oid = repo.raw().merge_base(*patch.target, *patch.oid)?;
let patch = patches
@@ -795,7 +800,7 @@ async fn patch_update_handler(
.signer()
.map_err(|_| Error::Auth("Unauthorized"))?;
let repo = storage.repository(project)?;
let mut patches = patch::Patches::open(&repo)?;
let mut patches = ctx.profile.patches_mut(&repo)?;
let mut patch = patches.get_mut(&patch_id.into())?;
let id = match action {
patch::Action::Edit { title, target } => patch.edit(title, target, &signer)?,
@@ -940,9 +945,9 @@ async fn patches_handler(
let state = state.unwrap_or_default();
let storage = &ctx.profile.storage;
let repo = storage.repository(project)?;
let patches = patch::Patches::open(&repo)?;
let patches = ctx.profile.patches(&repo)?;
let mut patches = patches
.all()?
.list()?
.filter_map(|r| {
let (id, patch) = r.ok()?;
(state.matches(patch.state())).then_some((id, patch))
@@ -968,9 +973,8 @@ async fn patch_handler(
) -> impl IntoResponse {
let storage = &ctx.profile.storage;
let repo = storage.repository(project)?;
let patch = patch::Patches::open(&repo)?
.get(&patch_id.into())?
.ok_or(Error::NotFound)?;
let patches = ctx.profile.patches(&repo)?;
let patch = patches.get(&patch_id.into())?.ok_or(Error::NotFound)?;
let aliases = ctx.profile.aliases();
Ok::<_, Error>(Json(api::json::patch(
+4 -6
View File
@@ -11,18 +11,16 @@ use serde_json::Value;
use time::OffsetDateTime;
use tower::ServiceExt;
use radicle::cob::issue::Issues;
use radicle::cob::patch::{MergeTarget, Patches};
use radicle::cob::patch::MergeTarget;
use radicle::crypto::ssh::keystore::MemorySigner;
use radicle::crypto::ssh::Keystore;
use radicle::crypto::{KeyPair, Seed, Signer};
use radicle::git::{raw as git2, RefString};
use radicle::identity::Visibility;
use radicle::node;
use radicle::profile;
use radicle::profile::Home;
use radicle::storage::ReadStorage;
use radicle::Storage;
use radicle::{node, profile};
use radicle_crypto::test::signer::MockSigner;
use crate::api::{auth, Context};
@@ -231,7 +229,7 @@ fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G
let storage = &profile.storage;
let repo = storage.repository(rid).unwrap();
let mut issues = Issues::open(&repo).unwrap();
let mut issues = profile.issues_mut(&repo).unwrap();
let issue = issues
.create(
"Issue #1".to_string(),
@@ -245,7 +243,7 @@ fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G
tracing::debug!(target: "test", "Contributor issue: {}", issue.id());
// eq. rad patch open
let mut patches = Patches::open(&repo).unwrap();
let mut patches = profile.patches_mut(&repo).unwrap();
let oid = radicle::git::Oid::from_str(HEAD).unwrap();
let base = radicle::git::Oid::from_str(PARENT).unwrap();
let patch = patches