feat: fetch from cache if existed

Signed-off-by: Pakin <pakin.t@forth.co.th>
This commit is contained in:
Pakin 2026-02-02 14:12:16 +07:00
parent 3043f30012
commit 03263815e6
2 changed files with 98 additions and 31 deletions

View file

@ -8,10 +8,10 @@ use axum::{
}; };
use axum_macros::debug_handler; use axum_macros::debug_handler;
use git2::{Cred, FetchOptions, PushOptions, RemoteCallbacks, Repository}; use git2::{Cred, FetchOptions, PushOptions, RemoteCallbacks, Repository};
use libtbr::{models, recipe_functions::common};
use log::{error, info, warn}; use log::{error, info, warn};
use redis::TypedCommands; use redis::TypedCommands;
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use serde_json::{Value, json}; use serde_json::{Value, json};
use tokio::sync::Mutex; use tokio::sync::Mutex;
@ -23,6 +23,8 @@ pub struct AppState {
configures: gcm::Configure, configures: gcm::Configure,
repo: Arc<Mutex<Repository>>, repo: Arc<Mutex<Repository>>,
redis: Arc<Mutex<redis::Client>>, redis: Arc<Mutex<redis::Client>>,
// save already fetched path, further calls should fetch from redis instead.
fetched: Vec<String>,
} }
impl AppState { impl AppState {
@ -45,11 +47,30 @@ struct CheckoutParams {
} }
async fn checkout_handler( async fn checkout_handler(
State(state): State<AppState>, State(mut state): State<AppState>,
Query(param): Query<CheckoutParams>, Query(param): Query<CheckoutParams>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let mut response: HashMap<String, Value> = HashMap::new(); let mut response: HashMap<String, Value> = HashMap::new();
// quick response from redis
if let Ok(mut rcli) = state.redis.try_lock()
&& state
.fetched
.contains(&param.path.clone().as_str().to_string())
{
if let Ok(res) = rcli.get(param.path.clone().as_str().to_string()) {
match res {
Some(res) => {
response.insert("result".to_string(), json!(res));
return (axum::http::StatusCode::OK, Json(json!(response)));
}
None => {
// continue
}
}
}
}
let repo_path = state.get_config("GIT_REPO_LOCAL_DEST"); let repo_path = state.get_config("GIT_REPO_LOCAL_DEST");
if repo_path.is_none() { if repo_path.is_none() {
response.insert( response.insert(
@ -105,12 +126,22 @@ async fn checkout_handler(
if let Some(blob) = obj.as_blob() { if let Some(blob) = obj.as_blob() {
let content = unsafe { str::from_utf8_unchecked(blob.content()) }; let content = unsafe { str::from_utf8_unchecked(blob.content()) };
response.insert("result".to_string(), json!(content.to_string())); response.insert("result".to_string(), json!(content.to_string()));
if let Ok(mut rcli) = state.redis.try_lock() {
let _ = rcli.set(legit_path.to_string(), content.to_string());
state.fetched.push(legit_path.to_string());
}
} else if let Some(tree) = obj.as_tree() { } else if let Some(tree) = obj.as_tree() {
let dir_list = tree let dir_list = tree
.iter() .iter()
.map(|x| x.name().unwrap_or("").to_string()) .map(|x| x.name().unwrap_or("").to_string())
.collect::<Vec<String>>(); .collect::<Vec<String>>();
response.insert("result".to_string(), json!(dir_list)); response.insert("result".to_string(), json!(dir_list));
if let Ok(mut rcli) = state.redis.try_lock() {
let _ = rcli.set(legit_path.to_string(), dir_list.join(","));
state.fetched.push(legit_path.to_string());
}
} else { } else {
let error_log = "not obj nor tree"; let error_log = "not obj nor tree";
error!("{error_log}"); error!("{error_log}");
@ -123,7 +154,7 @@ async fn checkout_handler(
async fn fetch_handler(State(state): State<AppState>) -> impl IntoResponse { async fn fetch_handler(State(state): State<AppState>) -> impl IntoResponse {
let mut response: HashMap<String, Value> = HashMap::new(); let mut response: HashMap<String, Value> = HashMap::new();
if let Some(repo_path) = state.get_config("GIT_REPO_LOCAL_DEST") { if let Some(repo_path) = state.clone().get_config("GIT_REPO_LOCAL_DEST") {
let rpath = repo_path.clone(); let rpath = repo_path.clone();
let repo = match Repository::open_bare(rpath) { let repo = match Repository::open_bare(rpath) {
@ -192,12 +223,33 @@ async fn fetch_handler(State(state): State<AppState>) -> impl IntoResponse {
); );
} }
tokio::spawn(async move {
let _ = broadcast_need_update_new_data_after_fetch(state.clone()).await;
});
( (
axum::http::StatusCode::OK, axum::http::StatusCode::OK,
Json(json!({"result": "fetch success"})), Json(json!({"result": "fetch success"})),
) )
} }
// refetch data after request `/fetch`
async fn broadcast_need_update_new_data_after_fetch(
state: AppState,
) -> Result<(), Box<dyn std::error::Error>> {
// clear out saved state, user must request again
// state.fetched.clear(); // ----> background fetch into redis
//
//
let state_cl = state.clone();
let mut rcli = state_cl.redis.try_lock()?;
for s in state.fetched {
let _ = rcli.publish("recipe_repo_news", s);
}
Ok(())
}
// { path: "/path/to/file", signature: { // { path: "/path/to/file", signature: {
// username: "", email: "" // username: "", email: ""
// }, patch_key: "edit_id_from_redis"} // }, patch_key: "edit_id_from_redis"}
@ -382,7 +434,7 @@ async fn apply_patch_to_file(
} }
} }
match serde_json::to_string(&current_full_map.clone()) { match serde_json::to_string_pretty(&current_full_map.clone()) {
Ok(ss) => ss, Ok(ss) => ss,
Err(_) => String::new(), Err(_) => String::new(),
} }
@ -521,6 +573,7 @@ pub async fn run(config: gcm::Configure) -> gcm::StandardResult {
config.get("REDIS_URI").unwrap_or(&"".to_string()), config.get("REDIS_URI").unwrap_or(&"".to_string()),
config.get("REDIS_PORT").unwrap_or(&"".to_string()) config.get("REDIS_PORT").unwrap_or(&"".to_string())
))?)), ))?)),
fetched: Vec::new(),
}; };
let app = Router::new() let app = Router::new()

View file

@ -4,7 +4,10 @@ use env_logger::Builder;
use libtbr::recipe_functions::common; use libtbr::recipe_functions::common;
use log::info; use log::info;
use crate::{git::setup_git_repo, reg::{heartbeat_loop, registry::registry_client::RegistryClient}}; use crate::{
git::setup_git_repo,
reg::{heartbeat_loop, registry::registry_client::RegistryClient},
};
mod app; mod app;
mod gcm; mod gcm;
@ -12,49 +15,60 @@ mod git;
mod reg; mod reg;
fn setup_log(config: gcm::Configure) -> gcm::StandardResult { fn setup_log(config: gcm::Configure) -> gcm::StandardResult {
let logfile = File::create(config.get("LOG_NAME").unwrap_or(&"run.log".to_string()))?; let logfile = File::create(config.get("LOG_NAME").unwrap_or(&"run.log".to_string()))?;
Builder::from_env(env_logger::Env::default().default_filter_or("debug")) Builder::from_env(env_logger::Env::default().default_filter_or("debug"))
.target(env_logger::Target::Pipe(Box::new(logfile))).init(); .target(env_logger::Target::Pipe(Box::new(logfile)))
.init();
Ok(())
Ok(())
} }
async fn setup_registry(config: gcm::Configure) -> gcm::StandardResult { async fn setup_registry(config: gcm::Configure) -> gcm::StandardResult {
let cfg_clone = config.clone(); let cfg_clone = config.clone();
let register_server_address = cfg_clone.get("REGISTRY_SERVER").expect("not found registry server"); let register_server_address = cfg_clone
let name = cfg_clone.get("SERVICE_NAME").expect("missing service name"); .get("REGISTRY_SERVER")
let url = cfg_clone.get("SERVICE_URL").expect("missing service url"); .expect("not found registry server");
let name = cfg_clone.get("SERVICE_NAME").expect("missing service name");
let url = cfg_clone.get("SERVICE_URL").expect("missing service url");
let token = cfg_clone.get("REGISTRY_TOKEN").expect("token not provided"); let token = cfg_clone.get("REGISTRY_TOKEN").expect("token not provided");
unsafe { unsafe {
env::set_var("REGISTRY_TOKEN", token); env::set_var("REGISTRY_TOKEN", token);
} }
let mut client = RegistryClient::connect(register_server_address.clone()).await?; let mut client = RegistryClient::connect(register_server_address.clone()).await?;
reg::register_service(&mut client, name, url).await; reg::register_service(&mut client, name, url).await;
tokio::spawn(heartbeat_loop(name.to_string(), url.to_string())); tokio::spawn(heartbeat_loop(name.to_string(), url.to_string()));
Ok(()) Ok(())
} }
#[tokio::main] #[tokio::main]
async fn main() -> gcm::StandardResult { async fn main() -> gcm::StandardResult {
let config: gcm::Configure = common::get_config(); let config: gcm::Configure = common::get_config();
setup_log(config.clone())?; setup_log(config.clone())?;
match std::fs::read_dir(config.get("GIT_REPO_LOCAL_DEST").expect("not exist")) { match std::fs::read_dir(config.get("GIT_REPO_LOCAL_DEST").expect("not exist")) {
Ok(_) => { Ok(_) => {
info!("GIT_REPO `{dest}` already setup", dest = config.get("GIT_REPO_LOCAL_DEST").unwrap()) info!(
}, "GIT_REPO `{dest}` already setup",
Err(_) => { dest = config.get("GIT_REPO_LOCAL_DEST").unwrap()
setup_git_repo(config.clone())?; )
} }
Err(_) => {
setup_git_repo(config.clone())?;
}
} }
info!(
"APP VERSION: {}",
config
.get("CONFIG_VERSION")
.expect("config version not defined")
);
setup_registry(config.clone()).await?; setup_registry(config.clone()).await?;
app::run(config.clone()).await?; app::run(config.clone()).await?;