Merge branch 'chore/extract-cache'

This commit is contained in:
Valentin Brandl 2019-08-07 22:24:47 +02:00
commit f874c336ef
No known key found for this signature in database
GPG Key ID: 30D341DD34118D7D
12 changed files with 299 additions and 112 deletions

View File

@ -17,17 +17,21 @@ RUN ./build.sh
FROM ekidd/rust-musl-builder:stable as backend FROM ekidd/rust-musl-builder:stable as backend
# create new cargo project # create new cargo project
RUN USER=rust cargo init --bin RUN USER=rust cargo new --bin gitache
RUN cargo new --lib time-cache
WORKDIR /home/rust/src/gitache
# copy build config # copy build config
COPY --chown=rust ./backend/Cargo.lock ./Cargo.lock COPY --chown=rust ./backend/Cargo.lock ./gitache/Cargo.lock
COPY --chown=rust ./backend/Cargo.toml ./Cargo.toml COPY --chown=rust ./backend/Cargo.toml ./gitache/Cargo.toml
COPY --chown=rust ./time-cache/Cargo.toml ./time-cache/Cargo.toml
# build to cache dependencies # build to cache dependencies
RUN cargo build --release RUN cargo build --release
# delete build cache to prevent caching issues later on # delete build cache to prevent caching issues later on
RUN rm -r ./target/x86_64-unknown-linux-musl/release/.fingerprint/gitache-* RUN rm -r ./target/x86_64-unknown-linux-musl/release/.fingerprint/gitache-*
COPY ./backend/static ./static COPY ./backend/static ./gitache/static
COPY ./backend/src ./src COPY ./backend/src ./gitache/src
COPY ./time-cache/src ./time-cache/src
# build source code # build source code
RUN cargo build --release RUN cargo build --release
@ -43,7 +47,7 @@ COPY --from=linuxkit/ca-certificates:v0.7 / /
COPY --from=user_builder /etc/passwd /etc/passwd COPY --from=user_builder /etc/passwd /etc/passwd
USER dummy USER dummy
COPY --from=backend /home/rust/src/target/x86_64-unknown-linux-musl/release/gitache / COPY --from=backend /home/rust/src/gitache/target/x86_64-unknown-linux-musl/release/gitache /
COPY --from=frontend /output/index.html /public/index.html COPY --from=frontend /output/index.html /public/index.html
COPY --from=frontend /output/scripts /public/scripts COPY --from=frontend /output/scripts /public/scripts
COPY --from=frontend /output/assets /public/assets COPY --from=frontend /output/assets /public/assets

5
backend/Cargo.lock generated
View File

@ -659,6 +659,7 @@ dependencies = [
"serde_derive 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
"structopt 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
"time-cache 0.1.0",
] ]
[[package]] [[package]]
@ -1602,6 +1603,10 @@ dependencies = [
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "time-cache"
version = "0.1.0"
[[package]] [[package]]
name = "tokio-codec" name = "tokio-codec"
version = "0.1.1" version = "0.1.1"

View File

@ -19,3 +19,4 @@ serde = { version = "1.0.97", features = ["rc"] }
serde_derive = "1.0.97" serde_derive = "1.0.97"
serde_json = "1.0.40" serde_json = "1.0.40"
structopt = "0.2.18" structopt = "0.2.18"
time-cache = { path = "../time-cache" }

View File

@ -1,90 +0,0 @@
use crate::statics::REDIRECT_AGE;
use std::{
collections::HashMap,
hash::Hash,
sync::Arc,
time::{Duration, Instant},
};
pub(crate) struct Cache<K, V> {
cache: HashMap<K, CacheEntry<V>>,
duration: Duration,
}
impl<K, V> Cache<K, V>
where
K: Eq + Hash,
{
pub(crate) fn new() -> Self {
Self {
cache: HashMap::new(),
duration: REDIRECT_AGE,
}
}
pub(crate) fn get(&self, key: &K) -> CacheResult<&V> {
if let Some(entry) = self.cache.get(key) {
if Self::is_valid(Instant::now(), entry) {
CacheResult::Cached(&entry.1)
} else {
CacheResult::Invalid
}
} else {
CacheResult::Empty
}
}
pub(crate) fn invalidate(&mut self, key: &K) -> bool {
self.cache.remove(key).is_some()
}
pub(crate) fn store(&mut self, key: K, value: V) -> Option<V> {
self.cache
.insert(key, CacheEntry::new(value, self.duration))
.map(|old| old.1)
}
pub(crate) fn clear(&mut self) {
let now = Instant::now();
self.cache.retain(|_, v| !Self::is_valid(now, v));
}
fn is_valid(when: Instant, entry: &CacheEntry<V>) -> bool {
entry.0 >= when
}
}
pub(crate) enum CacheResult<T> {
Cached(T),
Invalid,
Empty,
}
struct CacheEntry<T>(Instant, T);
impl<T> CacheEntry<T> {
fn new(value: T, duration: Duration) -> Self {
CacheEntry(Instant::now() + duration, value)
}
}
#[derive(Eq, PartialEq, Hash, Debug)]
pub(crate) struct Key(Service, Arc<String>, Arc<String>, Arc<String>);
#[derive(Eq, PartialEq, Hash, Debug)]
pub(crate) enum Service {
GitHub,
GitLab,
Bitbucket,
}
impl Key {
pub(crate) fn new(
service: Service,
user: Arc<String>,
repo: Arc<String>,
branch: Arc<String>,
) -> Self {
Key(service, user, repo, branch)
}
}

View File

@ -1,8 +1,6 @@
use crate::{ use crate::service;
cache::{Cache, Key},
service::Service,
};
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use time_cache::Cache;
pub(crate) type State = Arc<RwLock<Cache<Key, String>>>; pub(crate) type State = Arc<RwLock<Cache<Key, String>>>;
@ -19,7 +17,7 @@ impl FilePath {
format!("{}/{}/{}/{}", self.user, self.repo, self.commit, self.file) format!("{}/{}/{}/{}", self.user, self.repo, self.commit, self.file)
} }
pub(crate) fn to_key<T: Service>(&self) -> Key { pub(crate) fn to_key<T: service::Service>(&self) -> Key {
Key::new( Key::new(
T::cache_service(), T::cache_service(),
self.user.clone(), self.user.clone(),
@ -28,3 +26,24 @@ impl FilePath {
) )
} }
} }
#[derive(Eq, PartialEq, Hash, Debug)]
pub(crate) struct Key(Service, Arc<String>, Arc<String>, Arc<String>);
#[derive(Eq, PartialEq, Hash, Debug)]
pub(crate) enum Service {
GitHub,
GitLab,
Bitbucket,
}
impl Key {
pub(crate) fn new(
service: Service,
user: Arc<String>,
repo: Arc<String>,
branch: Arc<String>,
) -> Self {
Key(service, user, repo, branch)
}
}

View File

@ -16,7 +16,6 @@ mod service;
mod statics; mod statics;
use crate::{ use crate::{
cache::{Cache, CacheResult},
cdn::Cloudflare, cdn::Cloudflare,
data::{FilePath, State}, data::{FilePath, State},
error::Result, error::Result,
@ -31,6 +30,7 @@ use actix_web::{
use awc::{http::StatusCode, Client}; use awc::{http::StatusCode, Client};
use futures::Future; use futures::Future;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use time_cache::{Cache, CacheResult};
fn proxy_file<T: Service>( fn proxy_file<T: Service>(
client: web::Data<Client>, client: web::Data<Client>,
@ -210,7 +210,7 @@ fn main() -> Result<()> {
pretty_env_logger::init(); pretty_env_logger::init();
openssl_probe::init_ssl_cert_env_vars(); openssl_probe::init_ssl_cert_env_vars();
let state: State = Arc::new(RwLock::new(Cache::new())); let state: State = Arc::new(RwLock::new(Cache::new(REDIRECT_AGE)));
Ok(HttpServer::new(move || { Ok(HttpServer::new(move || {
App::new() App::new()
.data(Client::new()) .data(Client::new())

View File

@ -1,6 +1,5 @@
use crate::{ use crate::{
cache, data::{self, FilePath, State},
data::{FilePath, State},
statics::{load_env_var, GITHUB_AUTH_QUERY, OPT, REDIRECT_AGE}, statics::{load_env_var, GITHUB_AUTH_QUERY, OPT, REDIRECT_AGE},
}; };
use actix_web::{ use actix_web::{
@ -71,7 +70,7 @@ pub(crate) trait Service: Sized {
fn raw_url(user: &str, repo: &str, commit: &str, file: &str) -> String; fn raw_url(user: &str, repo: &str, commit: &str, file: &str) -> String;
fn cache_service() -> cache::Service; fn cache_service() -> data::Service;
fn api_url(path: &FilePath) -> String; fn api_url(path: &FilePath) -> String;
@ -141,8 +140,8 @@ impl Github {
impl Service for Github { impl Service for Github {
type Response = GitHubApiResponse; type Response = GitHubApiResponse;
fn cache_service() -> cache::Service { fn cache_service() -> data::Service {
cache::Service::GitHub data::Service::GitHub
} }
fn path() -> &'static str { fn path() -> &'static str {
@ -176,8 +175,8 @@ pub(crate) struct Bitbucket;
impl Service for Bitbucket { impl Service for Bitbucket {
type Response = BitbucketApiResponse; type Response = BitbucketApiResponse;
fn cache_service() -> cache::Service { fn cache_service() -> data::Service {
cache::Service::Bitbucket data::Service::Bitbucket
} }
fn path() -> &'static str { fn path() -> &'static str {
@ -208,8 +207,8 @@ pub(crate) struct GitLab;
impl Service for GitLab { impl Service for GitLab {
type Response = GitLabApiResponse; type Response = GitLabApiResponse;
fn cache_service() -> cache::Service { fn cache_service() -> data::Service {
cache::Service::GitLab data::Service::GitLab
} }
fn path() -> &'static str { fn path() -> &'static str {

1
time-cache/.dockerignore Symbolic link
View File

@ -0,0 +1 @@
.gitignore

3
time-cache/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock

6
time-cache/Cargo.lock generated Normal file
View File

@ -0,0 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "time-cache"
version = "0.1.0"

9
time-cache/Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "time-cache"
version = "0.1.0"
authors = ["Valentin Brandl <vbrandl@riseup.net>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

230
time-cache/src/lib.rs Normal file
View File

@ -0,0 +1,230 @@
//! Simple cache structure that stores values for a specified time. The cache itself is backed by
//! a HashMap.
use std::{
collections::HashMap,
hash::Hash,
time::{Duration, Instant},
};
/// Time based cache, that stores values for a defined time.
pub struct Cache<K, V> {
cache: HashMap<K, CacheEntry<V>>,
duration: Duration,
}
impl<K, V> Cache<K, V>
where
K: Eq + Hash,
{
/// Creates a new cache.
///
/// # Example
/// ```
/// use std::time::Duration;
/// use time_cache::{Cache, CacheResult};
///
/// let cache: Cache<u8, u8> = Cache::new(Duration::from_secs(0));
/// assert_eq!(CacheResult::Empty, cache.get(&0));
/// ```
pub fn new(duration: Duration) -> Self {
Self {
cache: HashMap::new(),
duration,
}
}
/// Get an item from the cache. The item can be either valid, invalid or non existent.
///
/// # Example
/// ```
/// use std::time::Duration;
/// use time_cache::{Cache, CacheResult};
///
/// let key = 0;
/// let value = 1;
/// let mut cache: Cache<u8, u8> = Cache::new(Duration::from_secs(0));
/// assert_eq!(CacheResult::Empty, cache.get(&key));
/// cache.store(key, value);
/// assert_eq!(CacheResult::Invalid, cache.get(&key));
/// ```
pub fn get(&self, key: &K) -> CacheResult<&V> {
if let Some(entry) = self.cache.get(key) {
if Self::is_valid(Instant::now(), entry) {
CacheResult::Cached(&entry.1)
} else {
CacheResult::Invalid
}
} else {
CacheResult::Empty
}
}
/// Removes an item from the cache. Returns `true` if the key was present.
///
/// # Example
/// ```
/// use std::time::Duration;
/// use time_cache::{Cache, CacheResult};
///
/// let key = 0;
/// let value = 1;
/// let mut cache: Cache<u8, u8> = Cache::new(Duration::from_secs(0));
/// assert!(!cache.invalidate(&key));
/// cache.store(key, value);
/// assert!(cache.invalidate(&key));
/// ```
pub fn invalidate(&mut self, key: &K) -> bool {
self.cache.remove(key).is_some()
}
/// Stores an item in the cache.
///
/// # Example
/// ```
/// use std::time::Duration;
/// use time_cache::{Cache, CacheResult};
///
/// let key = 0;
/// let value = 1;
/// let dur = Duration::from_millis(500);
/// let mut cache: Cache<u8, u8> = Cache::new(dur);
///
/// assert_eq!(CacheResult::Empty, cache.get(&key));
/// cache.store(key, value);
/// assert_eq!(CacheResult::Cached(&value), cache.get(&key));
/// std::thread::sleep(dur);
/// assert_eq!(CacheResult::Invalid, cache.get(&key));
/// ```
pub fn store(&mut self, key: K, value: V) -> Option<V> {
self.cache
.insert(key, CacheEntry::new(value, self.duration))
.map(|old| old.1)
}
/// Removes all invalid items from the cache.
///
/// # Example
/// ```
/// use std::time::Duration;
/// use time_cache::{Cache, CacheResult};
///
/// let key = 0;
/// let value = 1;
/// let dur = Duration::from_secs(0);
/// let mut cache: Cache<u8, u8> = Cache::new(dur);
///
/// assert_eq!(CacheResult::Empty, cache.get(&key));
/// cache.store(key, value);
/// assert_eq!(CacheResult::Invalid, cache.get(&key));
/// cache.clear();
/// assert_eq!(CacheResult::Empty, cache.get(&key));
/// ```
pub fn clear(&mut self) {
let now = Instant::now();
self.cache.retain(|_, v| Self::is_valid(now, v))
}
fn is_valid(when: Instant, entry: &CacheEntry<V>) -> bool {
entry.0 >= when
}
}
/// Result when requesting a cached item.
#[derive(Debug, PartialEq)]
pub enum CacheResult<T> {
/// Item is cached and still valid
Cached(T),
/// Item is cached but invalid
Invalid,
/// Item is not in the cache
Empty,
}
struct CacheEntry<T>(Instant, T);
impl<T> CacheEntry<T> {
fn new(value: T, duration: Duration) -> Self {
CacheEntry(Instant::now() + duration, value)
}
}
#[cfg(test)]
mod tests {
use super::{Cache, CacheResult};
use std::time::Duration;
#[test]
fn always_invalid() {
let key = 0;
let value = 1;
let mut cache = Cache::new(Duration::from_secs(0));
assert_eq!(CacheResult::Empty, cache.get(&key));
cache.store(key, value);
assert_eq!(CacheResult::Invalid, cache.get(&key));
}
#[test]
fn valid() {
let key = 0;
let value = 1;
let mut cache = Cache::new(Duration::from_secs(100));
assert_eq!(CacheResult::Empty, cache.get(&key));
cache.store(key, value);
assert_eq!(CacheResult::Cached(&value), cache.get(&key));
}
#[test]
fn wait_for_invalidation() {
let key = 0;
let value = 1;
let dur = Duration::from_millis(500);
let mut cache = Cache::new(dur);
assert_eq!(CacheResult::Empty, cache.get(&key));
cache.store(key, value);
assert_eq!(CacheResult::Cached(&value), cache.get(&key));
std::thread::sleep(dur);
assert_eq!(CacheResult::Invalid, cache.get(&key));
}
#[test]
fn invalidate() {
let key = 0;
let value = 1;
let dur = Duration::from_secs(100);
let mut cache = Cache::new(dur);
assert_eq!(CacheResult::Empty, cache.get(&key));
cache.store(key, value);
assert_eq!(CacheResult::Cached(&value), cache.get(&key));
assert!(cache.invalidate(&key));
assert_eq!(CacheResult::Empty, cache.get(&key));
}
#[test]
fn invalidate_wait() {
let key = 0;
let value = 1;
let dur = Duration::from_millis(500);
let mut cache = Cache::new(dur);
assert_eq!(CacheResult::Empty, cache.get(&key));
cache.store(key, value);
assert_eq!(CacheResult::Cached(&value), cache.get(&key));
std::thread::sleep(dur);
assert_eq!(CacheResult::Invalid, cache.get(&key));
assert!(cache.invalidate(&key));
assert_eq!(CacheResult::Empty, cache.get(&key));
}
#[test]
fn clear() {
let key = 0;
let value = 1;
let dur = Duration::from_secs(0);
let mut cache = Cache::new(dur);
assert_eq!(CacheResult::Empty, cache.get(&key));
cache.store(key, value);
assert_eq!(CacheResult::Invalid, cache.get(&key));
cache.clear();
assert_eq!(CacheResult::Empty, cache.get(&key));
}
}