use std::time::Duration;
use hydracache::{CacheKeyBuilder, CacheOptions, RefreshOptions, TagSet};
use crate::{
CacheEntity, DeclaredLintMode, DeclaredRelation, DimensionAllow, DimensionProfile,
DimensionValidationMode, LintFinding, PolicyLintMetadata, ProfileValidation,
};
const SHORT_LIVED_TTL: Duration = Duration::from_secs(30);
const READ_MOSTLY_TTL: Duration = Duration::from_secs(300);
const PER_ENTITY_TTL: Duration = Duration::from_secs(300);
const NEGATIVE_CACHE_TTL: Duration = Duration::from_secs(30);
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct QueryCachePolicy {
name: Option<String>,
key: Option<String>,
tags: TagSet,
ttl: Option<Duration>,
refresh: Option<RefreshOptions>,
required_dimensions: Vec<String>,
key_dimension_labels: Vec<String>,
tag_dimension_labels: Vec<String>,
dimension_profile: Option<DimensionProfile>,
dimension_validation_mode: DimensionValidationMode,
dimension_allow: Vec<DimensionAllow>,
lint_metadata: Option<PolicyLintMetadata>,
}
impl QueryCachePolicy {
pub fn new() -> Self {
Self::default()
}
pub fn short_lived() -> Self {
Self::new().ttl(SHORT_LIVED_TTL)
}
pub fn read_mostly() -> Self {
Self::new().ttl(READ_MOSTLY_TTL)
}
pub fn per_entity() -> Self {
Self::new().ttl(PER_ENTITY_TTL)
}
pub fn no_ttl_explicit_invalidation() -> Self {
Self::new()
}
pub fn negative_cache() -> Self {
Self::new().ttl(NEGATIVE_CACHE_TTL)
}
pub fn named(name: impl Into<String>) -> Self {
Self::new().with_name(name)
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn key_value(&self) -> Option<&str> {
self.key.as_deref()
}
pub fn tags_value(&self) -> &[String] {
self.tags.as_slice()
}
pub fn ttl_value(&self) -> Option<Duration> {
self.ttl
}
pub fn refresh_policy_value(&self) -> Option<RefreshOptions> {
self.refresh
}
pub fn required_dimensions_value(&self) -> &[String] {
&self.required_dimensions
}
pub fn key_dimension_labels(&self) -> &[String] {
&self.key_dimension_labels
}
pub fn tag_dimension_labels(&self) -> &[String] {
&self.tag_dimension_labels
}
pub fn dimension_profile(&self) -> Option<&DimensionProfile> {
self.dimension_profile.as_ref()
}
pub fn dimension_validation_mode(&self) -> DimensionValidationMode {
self.dimension_validation_mode
}
pub fn lint_metadata(&self) -> Option<&PolicyLintMetadata> {
self.lint_metadata.as_ref()
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn key_builder(self, key: CacheKeyBuilder) -> Self {
self.key(key.build_string())
}
pub fn for_entity(mut self, kind: impl ToString, id: impl ToString) -> Self {
let key = entity_key(kind, id);
self.key = Some(key.clone());
self.tags = self.tags.tag(key);
self
}
pub fn for_cache_entity<T>(mut self, id: T::Id) -> Self
where
T: CacheEntity,
{
let key = T::cache_key_for(&id);
self.key = Some(key);
self.tags = self.tags.tag(T::entity_tag_for(&id));
self.tags = append_optional_tag(self.tags, T::collection_tag());
self
}
pub fn collection(mut self, name: impl ToString) -> Self {
let tag = collection_tag(name);
self.key = Some(tag.clone());
self.tags = self.tags.tag(tag);
self
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tags = self.tags.tag(tag);
self
}
pub fn collection_tag(mut self, name: impl ToString) -> Self {
self.tags = self.tags.tag(collection_tag(name));
self
}
pub fn tags<I, S>(mut self, tags: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.tags = self.tags.tags(tags);
self
}
pub fn tag_set(mut self, tags: TagSet) -> Self {
self.tags = tags;
self
}
pub fn ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
pub fn refresh_policy(mut self, refresh: RefreshOptions) -> Self {
self.refresh = Some(refresh);
self
}
pub fn required_dimensions<I, S>(mut self, dimensions: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.required_dimensions = dimensions.into_iter().map(Into::into).collect();
self
}
pub fn required_dimension(mut self, dimension: impl Into<String>) -> Self {
self.required_dimensions.push(dimension.into());
self
}
pub fn with_key_dimension_labels<I, S>(mut self, labels: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.key_dimension_labels = labels.into_iter().map(Into::into).collect();
self
}
pub fn with_tag_dimension_labels<I, S>(mut self, labels: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.tag_dimension_labels = labels.into_iter().map(Into::into).collect();
self
}
pub fn with_dimension_profile(mut self, profile: DimensionProfile) -> Self {
let required = profile
.requirements()
.into_iter()
.map(|requirement| requirement.label().to_owned());
self.required_dimensions.extend(required);
self.dimension_profile = Some(profile);
self
}
pub fn with_dimension_validation_mode(mut self, mode: DimensionValidationMode) -> Self {
self.dimension_validation_mode = mode;
self
}
pub fn allow_dimension_violation(
mut self,
label: impl Into<String>,
reason: impl Into<String>,
) -> std::result::Result<Self, crate::DimensionAllowError> {
self.dimension_allow
.push(DimensionAllow::new(label, reason)?);
Ok(self)
}
pub fn validate_dimension_profile(&self) -> ProfileValidation {
let Some(profile) = &self.dimension_profile else {
return ProfileValidation::Pass;
};
let mut missing = Vec::new();
let mut unlinked = Vec::new();
for requirement in profile.requirements() {
let label = requirement.label();
if !self.key_dimension_labels.iter().any(|known| known == label) {
missing.push(label.to_owned());
} else if requirement.require_key_tag_link()
&& !self.tag_dimension_labels.iter().any(|known| known == label)
{
unlinked.push(label.to_owned());
}
}
let status = if !missing.is_empty() {
ProfileValidation::MissingDimensions(missing)
} else if !unlinked.is_empty() {
ProfileValidation::UnlinkedDimensions(unlinked)
} else {
ProfileValidation::Pass
};
self.apply_dimension_allow(status)
}
pub fn enforce_dimension_profile(&self) -> crate::Result<()> {
let status = self.validate_dimension_profile();
if self.dimension_validation_mode == DimensionValidationMode::Deny && !status.is_pass() {
return Err(hydracache::CacheError::Backend(format!(
"query cache policy dimension profile violation: {status}"
))
.into());
}
Ok(())
}
pub fn lint_sql(mut self, sql: impl Into<String>) -> Self {
self.lint_metadata_mut().sql = Some(sql.into());
self
}
pub fn dependency_lint_mode(mut self, mode: DeclaredLintMode) -> Self {
self.lint_metadata_mut().mode = mode;
self
}
pub fn declared_dependency(mut self, relation: DeclaredRelation) -> Self {
self.lint_metadata_mut().declared.push(relation);
self
}
pub fn declared_dependencies<I>(mut self, relations: I) -> Self
where
I: IntoIterator<Item = DeclaredRelation>,
{
self.lint_metadata_mut().declared.extend(relations);
self
}
pub fn lint_allow(mut self, finding: LintFinding, reason: impl Into<String>) -> Self {
self.lint_metadata_mut()
.suppressions
.push(crate::LintSuppression::new(finding, reason));
self
}
pub(crate) fn cache_options(&self) -> CacheOptions {
let mut options = CacheOptions::new().tag_set(self.tags.clone());
if let Some(ttl) = self.ttl {
options = options.ttl(ttl);
}
options
}
fn lint_metadata_mut(&mut self) -> &mut PolicyLintMetadata {
self.lint_metadata
.get_or_insert_with(PolicyLintMetadata::default)
}
fn apply_dimension_allow(&self, status: ProfileValidation) -> ProfileValidation {
let labels = match &status {
ProfileValidation::MissingDimensions(labels)
| ProfileValidation::UnlinkedDimensions(labels) => labels,
_ => return status,
};
let Some(allow) = self
.dimension_allow
.iter()
.find(|allow| labels.iter().any(|label| label == allow.label()))
else {
return status;
};
ProfileValidation::Allowed {
status: Box::new(status),
reason: allow.reason().to_owned(),
}
}
}
pub(crate) fn entity_key(kind: impl ToString, id: impl ToString) -> String {
CacheKeyBuilder::new().entity(kind, id).build_string()
}
pub(crate) fn collection_tag(name: impl ToString) -> String {
CacheKeyBuilder::from_segment(name).build_string()
}
fn append_optional_tag(tags: TagSet, tag: Option<String>) -> TagSet {
match tag {
Some(tag) => tags.tag(tag),
None => tags,
}
}