Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(turbo): add cache flag #9348

Merged
merged 14 commits into from
Nov 5, 2024
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/turborepo-cache/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ bytes.workspace = true
camino = { workspace = true }
futures = { workspace = true }
hmac = "0.12.1"
miette = { workspace = true }
os_str_bytes = "6.5.0"
path-clean = { workspace = true }
petgraph = "0.6.3"
Expand Down
42 changes: 32 additions & 10 deletions crates/turborepo-cache/src/async_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ mod tests {

use crate::{
test_cases::{get_test_cases, TestCase},
AsyncCache, CacheHitMetadata, CacheOpts, CacheSource, RemoteCacheOpts,
AsyncCache, CacheActions, CacheConfig, CacheHitMetadata, CacheOpts, CacheSource,
RemoteCacheOpts,
};

#[tokio::test]
Expand Down Expand Up @@ -255,9 +256,16 @@ mod tests {

let opts = CacheOpts {
cache_dir: Utf8PathBuf::from(".turbo/cache"),
remote_cache_read_only: false,
skip_remote: false,
skip_filesystem: true,
cache: CacheConfig {
local: CacheActions {
read: false,
write: false,
},
remote: CacheActions {
read: true,
write: true,
},
},
workers: 10,
remote_cache_opts: Some(RemoteCacheOpts {
unused_team_id: Some("my-team".to_string()),
Expand Down Expand Up @@ -337,9 +345,16 @@ mod tests {

let opts = CacheOpts {
cache_dir: Utf8PathBuf::from(".turbo/cache"),
remote_cache_read_only: false,
skip_remote: true,
skip_filesystem: false,
cache: CacheConfig {
local: CacheActions {
read: true,
write: true,
},
remote: CacheActions {
read: false,
write: false,
},
},
workers: 10,
remote_cache_opts: Some(RemoteCacheOpts {
unused_team_id: Some("my-team".to_string()),
Expand Down Expand Up @@ -429,9 +444,16 @@ mod tests {

let opts = CacheOpts {
cache_dir: Utf8PathBuf::from(".turbo/cache"),
remote_cache_read_only: false,
skip_remote: false,
skip_filesystem: false,
cache: CacheConfig {
local: CacheActions {
read: true,
write: true,
},
remote: CacheActions {
read: true,
write: true,
},
},
workers: 10,
remote_cache_opts: Some(RemoteCacheOpts {
unused_team_id: Some("my-team".to_string()),
Expand Down
231 changes: 231 additions & 0 deletions crates/turborepo-cache/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
use std::str::FromStr;

use miette::{Diagnostic, SourceSpan};
use thiserror::Error;

use crate::{CacheActions, CacheConfig};

#[derive(Debug, Error, Diagnostic, PartialEq)]
pub enum Error {
#[error("keys cannot be duplicated, found `{key}` multiple times")]
DuplicateKeys {
#[source_code]
text: String,
key: &'static str,
#[label]
span: Option<SourceSpan>,
},
#[error("actions cannot be duplicated, found `{action}` multiple times")]
DuplicateActions {
#[source_code]
text: String,
action: &'static str,
#[label]
span: Option<SourceSpan>,
},
#[error("invalid cache type and action pair, found `{pair}`, expected colon separated pair")]
InvalidCacheTypeAndAction {
#[source_code]
text: String,
pair: String,
#[label]
span: Option<SourceSpan>,
},
#[error("invalid cache action `{c}`")]
InvalidCacheAction {
#[source_code]
text: String,
c: char,
#[label]
span: Option<SourceSpan>,
},
#[error("invalid cache type `{s}`, expected `local` or `remote`")]
InvalidCacheType {
#[source_code]
text: String,
s: String,
#[label]
span: Option<SourceSpan>,
},
}

impl Error {
pub fn add_text(mut self, new_text: impl Into<String>) -> Self {
match &mut self {
Self::DuplicateKeys { text, .. } => *text = new_text.into(),
Self::DuplicateActions { text, .. } => *text = new_text.into(),
Self::InvalidCacheTypeAndAction { text, .. } => *text = new_text.into(),
Self::InvalidCacheAction { text, .. } => *text = new_text.into(),
Self::InvalidCacheType { text, .. } => *text = new_text.into(),
}

self
}

pub fn add_span(mut self, new_span: SourceSpan) -> Self {
match &mut self {
Self::DuplicateKeys { span, .. } => *span = Some(new_span),
Self::DuplicateActions { span, .. } => *span = Some(new_span),
Self::InvalidCacheTypeAndAction { span, .. } => *span = Some(new_span),
Self::InvalidCacheAction { span, .. } => *span = Some(new_span),
Self::InvalidCacheType { span, .. } => *span = Some(new_span),
}

self
}
}

impl FromStr for CacheConfig {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut cache = CacheConfig {
local: CacheActions {
read: false,
write: false,
},
remote: CacheActions {
read: false,
write: false,
},
};

if s.is_empty() {
return Ok(cache);
}

let mut seen_local = false;
let mut seen_remote = false;
let mut idx = 0;

for action in s.split(',') {
let (key, value) = action
.split_once(':')
.ok_or(Error::InvalidCacheTypeAndAction {
text: s.to_string(),
pair: action.to_string(),
span: Some(SourceSpan::new(idx.into(), action.len().into())),
})?;

match key {
"local" => {
if seen_local {
return Err(Error::DuplicateKeys {
text: s.to_string(),
key: "local",
span: Some(SourceSpan::new(idx.into(), key.len().into())),
});
}

seen_local = true;
cache.local = CacheActions::from_str(value).map_err(|err| {
err.add_text(s).add_span(SourceSpan::new(
(idx + key.len() + 1).into(),
key.len().into(),
))
})?;
}
"remote" => {
if seen_remote {
return Err(Error::DuplicateKeys {
text: s.to_string(),
key: "remote",
span: Some(SourceSpan::new(idx.into(), key.len().into())),
});
}

seen_remote = true;
cache.remote = CacheActions::from_str(value).map_err(|err| {
err.add_text(s).add_span(SourceSpan::new(
(idx + key.len() + 1).into(),
value.len().into(),
))
})?
}
ty => {
return Err(Error::InvalidCacheType {
text: s.to_string(),
s: ty.to_string(),
span: Some(SourceSpan::new(idx.into(), ty.len().into())),
})
}
}

idx += action.len() + 1;
}
Ok(cache)
}
}

impl FromStr for CacheActions {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut cache = CacheActions {
read: false,
write: false,
};

for c in s.chars() {
match c {
'r' => {
if cache.read {
return Err(Error::DuplicateActions {
text: s.to_string(),
action: "r (read)",
span: None,
});
}
cache.read = true;
}

'w' => {
if cache.write {
return Err(Error::DuplicateActions {
text: s.to_string(),
action: "w (write)",
span: None,
});
}
cache.write = true;
}
_ => {
return Err(Error::InvalidCacheAction {
c,
text: String::new(),
span: None,
})
}
}
}

Ok(cache)
}
}

#[cfg(test)]
mod test {
use test_case::test_case;

use super::*;

#[test_case("local:r,remote:w", Ok(CacheConfig { local: CacheActions { read: true, write: false }, remote: CacheActions { read: false, write: true } }) ; "local:r,remote:w"
)]
#[test_case("local:r", Ok(CacheConfig { local: CacheActions { read: true, write: false }, remote: CacheActions { read: false, write: false } }) ; "local:r"
)]
#[test_case("local:", Ok(CacheConfig { local: CacheActions { read: false, write: false }, remote: CacheActions { read: false, write: false } }) ; "empty action"
)]
#[test_case("local:,remote:", Ok(CacheConfig { local: CacheActions { read: false, write: false }, remote: CacheActions { read: false, write: false } }) ; "multiple empty actions"
)]
#[test_case("local:,remote:r", Ok(CacheConfig { local: CacheActions { read: false, write: false }, remote: CacheActions { read: true, write: false } }) ; "local: empty, remote:r"
)]
#[test_case("", Ok(CacheConfig { local: CacheActions { read: false, write: false }, remote: CacheActions { read: false, write: false } }) ; "empty"
)]
#[test_case("local:r,local:w", Err(Error::DuplicateKeys { text: "local:r,local:w".to_string(), key: "local", span: Some(SourceSpan::new(8.into(), 5.into())) }) ; "duplicate local key"
)]
#[test_case("local:rr", Err(Error::DuplicateActions { text: "local:rr".to_string(), action: "r (read)", span: Some(SourceSpan::new(6.into(), 5.into())) }) ; "duplicate action")]
#[test_case("remote:r,local=rx", Err(Error::InvalidCacheTypeAndAction { text: "remote:r,local=rx".to_string(), pair: "local=rx".to_string(), span: Some(SourceSpan::new(9.into(), 8.into())) }) ; "invalid key action pair")]
#[test_case("local:rx", Err(Error::InvalidCacheAction { c: 'x', text: "local:rx".to_string(), span: Some(SourceSpan::new(6.into(), 5.into())) }) ; "invalid action")]
#[test_case("file:r", Err(Error::InvalidCacheType { s: "file".to_string(), text: "file:r".to_string(), span: Some(SourceSpan::new(0.into(), 4.into())) }) ; "invalid cache type")]
fn test_cache_config(s: &str, expected: Result<CacheConfig, Error>) {
assert_eq!(CacheConfig::from_str(s), expected);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not blocking at all, but it might be nice to snapshot the error messages, easier to update if we change the tests/logic at all. There's a JSON reporter that makes assert_json_snapshot easy to use: https://github.com/vercel/turborepo/blob/main/crates/turborepo-lib/src/engine/builder.rs#L1358

}
}
42 changes: 38 additions & 4 deletions crates/turborepo-cache/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
mod async_cache;
/// The core cache creation and restoration logic.
pub mod cache_archive;
pub mod config;
/// File system cache
pub mod fs;
/// Remote cache
Expand Down Expand Up @@ -61,6 +62,8 @@ pub enum CacheError {
LinkTargetDoesNotExist(String, #[backtrace] Backtrace),
#[error("Invalid tar, link target does not exist on header")]
LinkTargetNotOnHeader(#[backtrace] Backtrace),
#[error(transparent)]
Config(#[from] config::Error),
#[error("attempted to restore unsupported file type: {0:?}")]
RestoreUnsupportedFileType(tar::EntryType, #[backtrace] Backtrace),
// We don't pass the `FileType` because there's no simple
Expand Down Expand Up @@ -105,12 +108,43 @@ pub struct CacheHitMetadata {
pub time_saved: u64,
}

#[derive(Clone, Debug, Default)]
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct CacheActions {
pub read: bool,
pub write: bool,
}

impl CacheActions {
pub fn should_use(&self) -> bool {
self.read || self.write
}
}

#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
pub struct CacheConfig {
pub local: CacheActions,
pub remote: CacheActions,
}

impl CacheConfig {
pub fn skip_writes(&self) -> bool {
!self.local.write && !self.remote.write
}
}

impl Default for CacheActions {
fn default() -> Self {
Self {
read: true,
write: true,
}
}
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CacheOpts {
pub cache_dir: Utf8PathBuf,
pub remote_cache_read_only: bool,
pub skip_remote: bool,
pub skip_filesystem: bool,
pub cache: CacheConfig,
pub workers: u32,
pub remote_cache_opts: Option<RemoteCacheOpts>,
}
Expand Down
Loading
Loading