#![deny(unsafe_code)]
use std::sync::Arc;
use anyhow::anyhow;
use async_trait::async_trait;
use tokio::{self, sync::oneshot, sync::RwLock};
use rmqtt::{
context::ServerContext,
hook::{Register, Type},
macros::Plugin,
plugin::{PackageInfo, Plugin},
register, Result,
};
use config::PluginConfig;
use flusher::{new_cache, start_flusher, start_recovery_loop, start_warmup, HistoryCaches};
mod api;
mod clients;
mod config;
mod embed;
mod flusher;
mod handler;
mod plugin;
mod prome;
mod subs;
mod types;
type ShutdownTX = oneshot::Sender<()>;
type PluginConfigType = Arc<RwLock<PluginConfig>>;
register!(HttpApiPlugin::new);
#[derive(Plugin)]
struct HttpApiPlugin {
scx: ServerContext,
register: Box<dyn Register>,
cfg: PluginConfigType,
shutdown_tx: Option<ShutdownTX>,
history_caches: Option<HistoryCaches>,
flush_handle: Option<tokio::task::JoinHandle<()>>,
}
impl HttpApiPlugin {
#[inline]
async fn new<S: Into<String>>(scx: ServerContext, name: S) -> Result<Self> {
let name = name.into();
let mut cfg = scx.plugins.read_config::<PluginConfig>(&name)?;
log::info!("{name} HttpApiPlugin cfg: {cfg:?}");
if let Some(ref mut storage_cfg) = cfg.storage {
let node_id_str = format!("{}", scx.node.id());
match storage_cfg.typ {
#[cfg(feature = "sled")]
rmqtt_storage::StorageType::Sled => {
storage_cfg.sled.path = storage_cfg.sled.path.replace("{node}", &node_id_str);
log::info!("{name} sled storage path: {}", storage_cfg.sled.path);
}
#[cfg(feature = "redis")]
rmqtt_storage::StorageType::Redis => {
storage_cfg.redis.prefix = storage_cfg.redis.prefix.replace("{node}", &node_id_str);
}
#[cfg(feature = "redis-cluster")]
rmqtt_storage::StorageType::RedisCluster => {
storage_cfg.redis_cluster.prefix =
storage_cfg.redis_cluster.prefix.replace("{node}", &node_id_str);
}
#[cfg(feature = "redb")]
rmqtt_storage::StorageType::Redb => {
storage_cfg.redb.path = storage_cfg.redb.path.replace("{node}", &node_id_str);
log::info!("{name} redb storage path: {}", storage_cfg.redb.path);
}
#[allow(unreachable_patterns)]
_ => return Err(anyhow!("Unsupported storage engine config")),
}
}
let interval_secs = cfg.flush_interval.as_secs().max(1);
let retention_secs = cfg.history_retention.as_secs().max(interval_secs);
let capacity = (retention_secs / interval_secs) as usize;
let stats_cache = new_cache(capacity)?;
let metrics_cache = new_cache(capacity)?;
let hc = HistoryCaches::new(stats_cache, metrics_cache);
let cfg = Arc::new(RwLock::new(cfg));
let register = scx.extends.hook_mgr().register();
let shutdown_tx = Some(Self::start(scx.clone(), cfg.clone(), Some(hc.clone())).await?);
Ok(Self { scx, register, cfg, shutdown_tx, history_caches: Some(hc), flush_handle: None })
}
async fn start(
scx: ServerContext,
cfg: PluginConfigType,
history_caches: Option<HistoryCaches>,
) -> Result<ShutdownTX> {
let (shutdown_tx, shutdown_rx) = oneshot::channel();
let (started_tx, started_rx) = oneshot::channel();
let http_laddr = cfg.read().await.http_laddr;
tokio::spawn(async move {
if let Err(e) =
api::listen_and_serve(scx, http_laddr, cfg, history_caches, shutdown_rx, started_tx).await
{
log::error!("{e}");
}
log::info!("Exit HTTP API Server, ..., http://{http_laddr:?}");
});
started_rx.await.map_err(|_| anyhow!("HTTP API server failed to bind on {http_laddr}"))?;
Ok(shutdown_tx)
}
}
#[async_trait]
impl Plugin for HttpApiPlugin {
#[inline]
async fn init(&mut self) -> Result<()> {
let name = self.name();
log::info!("{} init", name);
if let Some(ref storage_cfg) = self.cfg.read().await.storage.clone() {
match rmqtt_storage::init_db(storage_cfg).await {
Ok(db) => {
let node_id = self.scx.node.id();
let retention = self.cfg.read().await.history_retention;
let flush_intv = self.cfg.read().await.flush_interval;
let hc = self.history_caches.as_ref().unwrap();
let fh = start_flusher(
self.scx.clone(),
db.clone(),
hc.stats.clone(),
hc.metrics.clone(),
flush_intv,
retention,
);
let rh = start_recovery_loop(
hc.stats.clone(),
hc.metrics.clone(),
db.clone(),
retention,
node_id,
);
let wh =
start_warmup(hc.stats.clone(), hc.metrics.clone(), db.clone(), node_id, retention);
let combined = tokio::spawn(async move {
let _ = tokio::join!(fh, rh, wh);
});
self.flush_handle = Some(combined);
}
Err(e) => {
log::error!("{name} init storage db error, disabling history: {e}");
}
}
}
let mgs_type = self.cfg.read().await.message_type;
let flush_ms = self.cfg.read().await.flush_interval.as_millis() as u64;
self.register
.add(
Type::GrpcMessageReceived,
Box::new(handler::HookHandler::new(
self.scx.clone(),
mgs_type,
self.history_caches.clone(),
flush_ms,
)),
)
.await;
Ok(())
}
#[inline]
async fn get_config(&self) -> Result<serde_json::Value> {
self.cfg.read().await.to_json()
}
#[inline]
async fn load_config(&mut self) -> Result<()> {
let new_cfg = self.scx.plugins.read_config::<PluginConfig>(self.name())?;
if !self.cfg.read().await.changed(&new_cfg) {
return Ok(());
}
let restart_enable = self.cfg.read().await.restart_enable(&new_cfg);
if restart_enable {
let new_cfg = Arc::new(RwLock::new(new_cfg));
match Self::start(self.scx.clone(), new_cfg.clone(), self.history_caches.clone()).await {
Ok(tx) => {
if let Some(old_tx) = self.shutdown_tx.take() {
let _ = old_tx.send(());
}
self.shutdown_tx = Some(tx);
self.cfg = new_cfg;
}
Err(e) => {
return Err(e);
}
}
} else {
*self.cfg.write().await = new_cfg;
}
log::debug!("load_config ok, {:?}", self.cfg);
Ok(())
}
#[inline]
async fn start(&mut self) -> Result<()> {
log::info!("{} start", self.name());
self.register.start().await;
Ok(())
}
#[inline]
async fn stop(&mut self) -> Result<bool> {
log::info!("{} stop", self.name());
if let Some(handle) = self.flush_handle.take() {
handle.abort();
}
Ok(false)
}
}