Skip to content

Commit

Permalink
feat: Implement smarter app template caching:
Browse files Browse the repository at this point in the history
* Always use the cache for a given period (1h)
* If cache is older than that, just fetch the first page, and compare
  if no changes, then can re-use the cache
* Otherwise, refetch all
  • Loading branch information
theduke committed Jun 11, 2024
1 parent aabb9c2 commit b8fec8a
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 78 deletions.
96 changes: 53 additions & 43 deletions lib/backend-api/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{
types::{
self, CreateNamespaceVars, DeployApp, DeployAppConnection, DeployAppVersion,
DeployAppVersionConnection, DnsDomain, GetAppTemplateFromSlugVariables,
GetAppTemplatesQueryVariables, GetCurrentUserWithAppsVars, GetDeployAppAndVersion,
GetAppTemplatesVars, GetCurrentUserWithAppsVars, GetDeployAppAndVersion,
GetDeployAppVersionsVars, GetNamespaceAppsVars, GetSignedUrlForPackageUploadVariables, Log,
LogStream, PackageVersionConnection, PublishDeployAppVars, PushPackageReleasePayload,
SignedUrl, TagPackageReleasePayload, UpsertDomainFromZoneFileVars,
Expand Down Expand Up @@ -78,64 +78,74 @@ pub async fn fetch_app_templates(
sort_by: Option<types::AppTemplatesSortBy>,
) -> Result<Option<types::AppTemplateConnection>, anyhow::Error> {
client
.run_graphql_strict(types::GetAppTemplatesQuery::build(
GetAppTemplatesQueryVariables {
category_slug,
first,
after,
sort_by,
},
))
.run_graphql_strict(types::GetAppTemplates::build(GetAppTemplatesVars {
category_slug,
first,
after,
sort_by,
}))
.await
.map(|r| r.get_app_templates)
}

/// Fetch all app templates by paginating through the responses.
///
/// Will fetch at most `max` templates.
pub async fn fetch_all_app_templates(
pub fn fetch_all_app_templates(
client: &WasmerClient,
page_size: i32,
max: usize,
sort_by: Option<types::AppTemplatesSortBy>,
) -> Result<Vec<types::AppTemplate>, anyhow::Error> {
let mut all = Vec::new();
let mut cursor = None;
) -> impl futures::Stream<Item = Result<Vec<types::AppTemplate>, anyhow::Error>> + '_ {
let vars = GetAppTemplatesVars {
category_slug: String::new(),
first: page_size,
sort_by,
after: None,
};

loop {
let con = client
.run_graphql_strict(types::GetAppTemplatesQuery::build(
GetAppTemplatesQueryVariables {
category_slug: String::new(),
first: page_size,
sort_by,
after: cursor.take(),
},
))
.await?
.get_app_templates
.context("backend did not return any data")?;
futures::stream::try_unfold(
Some(vars),
move |vars: Option<types::GetAppTemplatesVars>| async move {
let vars = match vars {
Some(vars) => vars,
None => return Ok(None),
};

if con.edges.is_empty() {
break;
}
let con = client
.run_graphql_strict(types::GetAppTemplates::build(vars.clone()))
.await?
.get_app_templates
.context("backend did not return any data")?;

let new = con.edges.into_iter().flatten().filter_map(|edge| edge.node);
all.extend(new);
let items = con
.edges
.into_iter()
.flatten()
.filter_map(|edge| edge.node)
.collect::<Vec<_>>();

let next_cursor = con
.page_info
.end_cursor
.filter(|_| con.page_info.has_next_page && all.len() < max);
let next_cursor = con
.page_info
.end_cursor
.filter(|_| con.page_info.has_next_page);

let next_vars = if let Some(after) = next_cursor {
Some(types::GetAppTemplatesVars {
after: Some(after),
..vars
})
} else {
None
};

if let Some(next) = next_cursor {
cursor = Some(next);
} else {
break;
}
}
let res: Result<
Option<(Vec<types::AppTemplate>, Option<types::GetAppTemplatesVars>)>,
anyhow::Error,
> = Ok(Some((items, next_vars)));

Ok(all)
res
},
)
}

/// Get a signed URL to upload packages.
Expand Down
14 changes: 7 additions & 7 deletions lib/backend-api/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ mod queries {

use super::schema;

#[derive(cynic::Scalar, Debug, Clone)]
#[derive(cynic::Scalar, Debug, Clone, PartialEq, Eq)]
pub struct DateTime(pub String);

impl TryFrom<OffsetDateTime> for DateTime {
Expand Down Expand Up @@ -157,17 +157,17 @@ mod queries {
Popular,
}

#[derive(cynic::QueryVariables, Debug)]
pub struct GetAppTemplatesQueryVariables {
#[derive(cynic::QueryVariables, Debug, Clone)]
pub struct GetAppTemplatesVars {
pub category_slug: String,
pub first: i32,
pub after: Option<String>,
pub sort_by: Option<AppTemplatesSortBy>,
}

#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetAppTemplatesQueryVariables")]
pub struct GetAppTemplatesQuery {
#[cynic(graphql_type = "Query", variables = "GetAppTemplatesVars")]
pub struct GetAppTemplates {
#[arguments(
categorySlug: $category_slug,
first: $first,
Expand All @@ -189,7 +189,7 @@ mod queries {
pub cursor: String,
}

#[derive(serde::Serialize, cynic::QueryFragment, Debug)]
#[derive(serde::Serialize, cynic::QueryFragment, PartialEq, Eq, Debug)]
pub struct AppTemplate {
#[serde(rename = "demoUrl")]
pub demo_url: String,
Expand All @@ -214,7 +214,7 @@ mod queries {
pub use_cases: Jsonstring,
}

#[derive(cynic::Scalar, Debug, Clone)]
#[derive(cynic::Scalar, Debug, Clone, PartialEq, Eq)]
#[cynic(graphql_type = "JSONString")]
pub struct Jsonstring(pub String);

Expand Down
84 changes: 56 additions & 28 deletions lib/cli/src/commands/app/create.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
//! Create a new Edge app.
use crate::{
commands::AsyncCliCommand,
opts::{ApiOpts, ItemFormatOpts, WasmerEnv},
utils::{load_package_manifest, prompts::PackageCheckMode},
};
use anyhow::Context;
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, Confirm, Select};
use is_terminal::IsTerminal;
use std::{
collections::HashMap,
env,
Expand All @@ -17,10 +8,21 @@ use std::{
str::FromStr,
time::Duration,
};

use anyhow::Context;
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, Confirm, Select};
use futures::stream::TryStreamExt;
use is_terminal::IsTerminal;
use wasmer_api::{types::AppTemplate, WasmerClient};
use wasmer_config::{app::AppConfigV1, package::PackageSource};

use super::{deploy::CmdAppDeploy, util::login_user};
use crate::{
commands::AsyncCliCommand,
opts::{ApiOpts, ItemFormatOpts, WasmerEnv},
utils::{load_package_manifest, prompts::PackageCheckMode},
};

async fn write_app_config(app_config: &AppConfigV1, dir: Option<PathBuf>) -> anyhow::Result<()> {
let raw_app_config = app_config.clone().to_yaml()?;
Expand Down Expand Up @@ -282,18 +284,13 @@ impl CmdAppCreate {
/// Returns an error if the cache file is older than the max age.
fn load_cached_templates(
path: &Path,
max_cache_age: std::time::Duration,
) -> Result<Vec<AppTemplate>, anyhow::Error> {
) -> Result<(Vec<AppTemplate>, std::time::Duration), anyhow::Error> {
let modified = path.metadata()?.modified()?;
let age = modified.elapsed()?;

if age > max_cache_age {
anyhow::bail!("cache has expired");
}

let data = std::fs::read_to_string(path)?;
match serde_json::from_str::<Vec<AppTemplate>>(&data) {
Ok(v) => Ok(v),
Ok(v) => Ok((v, age)),
Err(err) => {
std::fs::remove_file(path).ok();
Err(err).context("could not deserialize cached file")
Expand Down Expand Up @@ -321,35 +318,65 @@ impl CmdAppCreate {
client: &WasmerClient,
cache_dir: &Path,
) -> Result<Vec<AppTemplate>, anyhow::Error> {
const MAX_CACHE_AGE: Duration = Duration::from_secs(60 * 15);
const MAX_CACHE_AGE: Duration = Duration::from_secs(60 * 60);
const MAX_COUNT: usize = 100;
const CACHE_FILENAME: &'static str = "app_templates.json";

let cache_path = cache_dir.join(CACHE_FILENAME);

match Self::load_cached_templates(&cache_path, MAX_CACHE_AGE) {
Ok(v) => {
return Ok(v);
let cached_items = match Self::load_cached_templates(&cache_path) {
Ok((items, age)) => {
if age <= MAX_CACHE_AGE {
return Ok(items);
}
items
}
Err(e) => {
tracing::trace!(error = &*e, "could not load templates from local cache");
Vec::new()
}
}
};

let templates = wasmer_api::query::fetch_all_app_templates(
// Either no cache present, or cache has exceeded max age.
// Fetch the first page.
// If first item matches, then no need to re-fetch.
let mut stream = Box::pin(wasmer_api::query::fetch_all_app_templates(
client,
10,
MAX_COUNT,
Some(wasmer_api::types::AppTemplatesSortBy::Popular),
)
.await?;
Some(wasmer_api::types::AppTemplatesSortBy::Newest),
));

let first_page = match stream.try_next().await? {
Some(items) => items,
None => return Ok(Vec::new()),
};

if let (Some(a), Some(b)) = (cached_items.first(), first_page.first()) {
if a == b {
// Cached items are up to date, no need to query more.
return Ok(cached_items);
}
}

let mut items = first_page;
while let Some(next) = stream.try_next().await? {
items.extend(next);

if items.len() >= MAX_COUNT {
break;
}
}

// Persist to cache.
if let Err(err) = Self::persist_template_cache(&cache_path, &templates) {
if let Err(err) = Self::persist_template_cache(&cache_path, &items) {
tracing::trace!(error = &*err, "could not persist template cache");
}

Ok(templates)
// TODO: sort items by popularity!
// Since we can't rely on backend sorting because of the cache
// preservation logic, the backend needs to add a popluarity field.

Ok(items)
}

// A utility function used to fetch the URL of the template to use.
Expand Down Expand Up @@ -395,6 +422,7 @@ impl CmdAppCreate {
let dialog = dialoguer::Select::with_theme(&theme)
.with_prompt(format!("Select a template ({} available)", items.len()))
.items(&items)
.max_length(10)
.clear(true)
.report(false)
.default(0);
Expand Down

0 comments on commit b8fec8a

Please sign in to comment.