Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use std::{collections::VecDeque, sync::Arc};

pub trait EstimatedSize {
fn estimated_size(&self) -> usize;
}

/// Simple LRU cache.
///
/// Evicts the Least Recently Used entry when space is needed.
pub struct LeastRecentlyUsedCache<K, V> {
size: usize,
capacity: usize,
entries: VecDeque<(K, Arc<V>)>,
}

impl<K, V> LeastRecentlyUsedCache<K, V> {
/// Creates the cache with a maxmimum capacity
pub fn new(capacity: usize) -> Self {
LeastRecentlyUsedCache {
size: 0,
capacity,
entries: VecDeque::default(),
}
}
}

#[cfg(test)]
impl<K, V> Default for LeastRecentlyUsedCache<K, V> {
fn default() -> Self {
Self::new(1 * 1024 * 1024)
}
}

impl<K: PartialEq<K>, V: EstimatedSize> LeastRecentlyUsedCache<K, V> {
/// Get a handle to the value.
///
/// Also move the entry in the cache to the first place.
pub(crate) fn get(&mut self, key: &K) -> Option<Arc<V>> {
if let Some(pos) = self.entries.iter().position(|(k, _)| k == key) {
// Move previously cached entry to the front
let entry = self.entries.remove(pos).unwrap();
self.entries.push_front(entry);
Some(self.entries[0].1.clone())
} else {
None
}
}

/// Inserts a new value to the cache
pub(crate) fn put(&mut self, key: K, value: Arc<V>) -> Arc<V> {
let estimated_size = value.estimated_size();

if estimated_size > self.capacity {
// Entry is too large, don't cache, return as is
return value;
}

// Remove duplicate or last entry when necessary
let removed = if let Some(pos) = self.entries.iter().position(|(k, _)| k == &key) {
self.entries.remove(pos)
} else if self.size + estimated_size >= self.capacity {
self.entries.pop_back()
} else {
None
};
if let Some(removed) = removed {
self.size -= removed.1.estimated_size();
}

// Add entry the front of the list and return it
self.size += estimated_size;
self.entries.push_front((key, value.clone()));
value
}

/// Removes a value from the cache
pub(crate) fn prune(&mut self, key: &K) -> bool {
if let Some(pos) = self.entries.iter().position(|(k, _)| k == key) {
let entry = self.entries.remove(pos).unwrap();
self.size -= entry.1.estimated_size();
true
} else {
false
}
}
}
70 changes: 13 additions & 57 deletions src/gh_comments.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::collections::VecDeque;
use std::fmt::Write;
use std::sync::Arc;
use std::time::Instant;
Expand All @@ -15,7 +14,10 @@ use hyper::{
header::{CACHE_CONTROL, CONTENT_SECURITY_POLICY, CONTENT_TYPE},
};

use crate::github::{GitHubGraphQlComment, GitHubIssueWithComments};
use crate::{
cache,
github::{GitHubGraphQlComment, GitHubIssueWithComments},
};
use crate::{
errors::AppError,
github::GitHubSimplifiedAuthor,
Expand All @@ -24,67 +26,21 @@ use crate::{
};

pub const STYLE_URL: &str = "/gh-comments/style@0.0.1.css";
pub const MARKDOWN_URL: &str = "/gh-comments/github-markdown@5.8.1.css";
pub const MARKDOWN_URL: &str = "/gh-comments/github-markdown@20260115.css";

const MAX_CACHE_CAPACITY_BYTES: u64 = 35 * 1024 * 1024; // 35 Mb
pub const GH_COMMENTS_CACHE_CAPACITY_BYTES: usize = 35 * 1024 * 1024; // 35 Mb

type CacheKey = (String, String, u64);

#[derive(Default)]
pub struct GitHubCommentsCache {
capacity: u64,
entries: VecDeque<(CacheKey, Arc<CachedComments>)>,
}
pub type GitHubCommentsCache = cache::LeastRecentlyUsedCache<(String, String, u64), CachedComments>;

pub struct CachedComments {
estimated_size: usize,
duration_secs: f64,
issue_with_comments: GitHubIssueWithComments,
}

impl GitHubCommentsCache {
pub fn get(&mut self, key: &CacheKey) -> Option<Arc<CachedComments>> {
if let Some(pos) = self.entries.iter().position(|(k, _)| k == key) {
// Move previously cached entry to the front
let entry = self.entries.remove(pos).unwrap();
self.entries.push_front(entry.clone());
Some(entry.1)
} else {
None
}
}

pub fn put(&mut self, key: CacheKey, value: Arc<CachedComments>) -> Arc<CachedComments> {
if value.estimated_size as u64 > MAX_CACHE_CAPACITY_BYTES {
// Entry is too large, don't cache, return as is
return value;
}

// Remove duplicate or last entry when necessary
let removed = if let Some(pos) = self.entries.iter().position(|(k, _)| k == &key) {
self.entries.remove(pos)
} else if self.capacity + value.estimated_size as u64 >= MAX_CACHE_CAPACITY_BYTES {
self.entries.pop_back()
} else {
None
};
if let Some(removed) = removed {
self.capacity -= removed.1.estimated_size as u64;
}

// Add entry the front of the list and return it
self.capacity += value.estimated_size as u64;
self.entries.push_front((key, value.clone()));
value
}

pub fn prune(&mut self, key: &CacheKey) -> bool {
if let Some(pos) = self.entries.iter().position(|(k, _)| k == key) {
self.entries.remove(pos);
true
} else {
false
}
impl cache::EstimatedSize for CachedComments {
fn estimated_size(&self) -> usize {
self.estimated_size
}
}

Expand Down Expand Up @@ -118,7 +74,7 @@ pub async fn gh_comments(
.github
.issue_with_comments(&owner, &repo, issue_id)
.await
.context("unable to fetch the issue and it's comments")?;
.context("unable to fetch the issue and it's comments (PRs are not yet supported)")?;

let duration = start.elapsed();
let duration_secs = duration.as_secs_f64();
Expand Down Expand Up @@ -229,7 +185,7 @@ pub async fn gh_comments(
headers.insert(
CONTENT_SECURITY_POLICY,
HeaderValue::from_static(
"default-src 'none'; script-src 'nonce-triagebot-gh-comments'; style-src 'self'; img-src *",
"default-src 'none'; script-src 'nonce-triagebot-gh-comments'; style-src 'self' 'unsafe-inline'; img-src *",
),
);

Expand All @@ -243,7 +199,7 @@ pub async fn style_css() -> impl IntoResponse {
}

pub async fn markdown_css() -> impl IntoResponse {
const MARKDOWN_CSS: &str = include_str!("gh_comments/github-markdown@5.8.1.css");
const MARKDOWN_CSS: &str = include_str!("gh_comments/github-markdown@20260115.css");

(immutable_headers("text/css; charset=utf-8"), MARKDOWN_CSS)
}
Expand Down
Loading