use crate::plugin::{Context, ExecPlugin, Plugin};
use crate::{RegisterExecPlugin, RegisterPlugin, Result};
use async_trait::async_trait;
use std::fmt;
use std::sync::Arc;
use tracing::{debug, info, warn};
use std::sync::RwLock;
#[derive(RegisterPlugin, RegisterExecPlugin)]
pub struct FallbackPlugin {
plugins: RwLock<Vec<Arc<dyn Plugin>>>,
pending: RwLock<Vec<String>>,
error_only: bool,
tag: Option<String>,
}
impl FallbackPlugin {
pub fn new(plugins: Vec<Arc<dyn Plugin>>) -> Self {
Self {
plugins: RwLock::new(plugins),
pending: RwLock::new(Vec::new()),
error_only: false,
tag: None,
}
}
pub fn with_names(names: Vec<String>) -> Self {
Self {
plugins: RwLock::new(Vec::new()),
pending: RwLock::new(names),
error_only: false,
tag: None,
}
}
pub fn error_only(mut self, error_only: bool) -> Self {
self.error_only = error_only;
self
}
pub fn resolve_children(&self, registry: &std::collections::HashMap<String, Arc<dyn Plugin>>) {
let mut pending = self.pending.write().unwrap();
if pending.is_empty() {
return;
}
let mut resolved = self.plugins.write().unwrap();
for name in pending.drain(..) {
if let Some(p) = registry.get(&name).cloned() {
debug!(plugin = %name, child = %p.display_name(), "Resolved fallback child");
resolved.push(p);
} else {
warn!(plugin = %name, "Fallback child plugin not found");
}
}
}
pub fn resolved_child_count(&self) -> usize {
self.plugins.read().unwrap().len()
}
pub fn pending_child_count(&self) -> usize {
self.pending.read().unwrap().len()
}
fn should_fallback(&self, ctx: &Context, had_error: bool) -> bool {
if had_error {
return true;
}
if self.error_only {
return false;
}
if let Some(response) = ctx.response() {
response.answers().is_empty()
} else {
true
}
}
}
impl fmt::Debug for FallbackPlugin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let resolved_count = self.plugins.read().unwrap().len();
let pending_count = self.pending.read().unwrap().len();
f.debug_struct("FallbackPlugin")
.field("resolved_children", &resolved_count)
.field("pending_children", &pending_count)
.field("error_only", &self.error_only)
.finish()
}
}
#[async_trait]
impl Plugin for FallbackPlugin {
fn name(&self) -> &str {
"fallback"
}
fn tag(&self) -> Option<&str> {
self.tag.as_deref()
}
async fn execute(&self, ctx: &mut Context) -> Result<()> {
let plugins = { self.plugins.read().unwrap().clone() };
debug!("Fallback: plugin count = {}", plugins.len());
debug!(
"Fallback children: {:?}",
plugins.iter().map(|p| p.display_name()).collect::<Vec<_>>()
);
for (i, plugin) in plugins.iter().enumerate() {
debug!(
"Fallback: trying plugin {} (index {})",
plugin.display_name(),
i
);
let had_error = match plugin.execute(ctx).await {
Ok(_) => false,
Err(e) => {
warn!(
plugin_index = i,
plugin_name = plugin.display_name(),
error = %e,
"Fallback: plugin failed"
);
true
}
};
if !self.should_fallback(ctx, had_error) {
debug!(
plugin_index = i,
plugin_name = plugin.display_name(),
"Fallback: plugin succeeded, stopping"
);
return Ok(());
}
if i < plugins.len() - 1 {
debug!(
plugin_index = i,
plugin_name = plugin.display_name(),
"Fallback: trying next plugin"
);
}
}
debug!("Fallback: all plugins attempted");
Ok(())
}
fn priority(&self) -> i32 {
100
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn init(config: &crate::config::types::PluginConfig) -> Result<std::sync::Arc<dyn Plugin>> {
let args = config.effective_args();
let primary = args
.get("primary")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let secondary = args
.get("secondary")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
info!(
"Creating fallback plugin (pending references): primary={}, secondary={}",
primary, secondary
);
let mut names = Vec::new();
if !primary.is_empty() {
names.push(primary);
}
if !secondary.is_empty() {
names.push(secondary);
}
Ok(Arc::new(FallbackPlugin {
plugins: RwLock::new(Vec::new()),
pending: RwLock::new(names),
error_only: false,
tag: config.tag.clone(),
}))
}
}
impl ExecPlugin for FallbackPlugin {
fn quick_setup(prefix: &str, exec_str: &str) -> Result<Arc<dyn Plugin>> {
if prefix != "fallback" {
return Err(crate::Error::Config(format!(
"ExecPlugin quick_setup: unsupported prefix '{}', expected 'fallback'",
prefix
)));
}
let names: Vec<String> = exec_str
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if names.is_empty() {
return Err(crate::Error::Config(
"fallback plugin requires at least one plugin name".to_string(),
));
}
let plugin = FallbackPlugin::with_names(names);
Ok(Arc::new(plugin))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dns::Message;
#[derive(Debug)]
struct TestPlugin {
name: String,
should_fail: bool,
should_empty: bool,
}
#[async_trait]
impl Plugin for TestPlugin {
fn name(&self) -> &str {
&self.name
}
async fn execute(&self, ctx: &mut Context) -> Result<()> {
if self.should_fail {
return Err(crate::Error::Plugin("test error".to_string()));
}
if !self.should_empty {
let mut response = Message::new();
use crate::dns::types::{RecordClass, RecordType};
use crate::dns::{RData, ResourceRecord};
response.add_answer(ResourceRecord::new(
"example.com".to_string(),
RecordType::A,
RecordClass::IN,
300,
RData::A("192.0.2.1".parse().unwrap()),
));
ctx.set_response(Some(response));
}
Ok(())
}
}
#[tokio::test]
async fn test_fallback_first_succeeds() {
let primary = Arc::new(TestPlugin {
name: "primary".to_string(),
should_fail: false,
should_empty: false,
});
let fallback_plugin = FallbackPlugin::new(vec![primary]);
let mut ctx = Context::new(Message::new());
fallback_plugin.execute(&mut ctx).await.unwrap();
assert!(ctx.response().is_some());
assert!(!ctx.response().unwrap().answers().is_empty());
}
#[tokio::test]
async fn test_fallback_on_error() {
let primary = Arc::new(TestPlugin {
name: "primary".to_string(),
should_fail: true,
should_empty: false,
});
let secondary = Arc::new(TestPlugin {
name: "secondary".to_string(),
should_fail: false,
should_empty: false,
});
let fallback_plugin = FallbackPlugin::new(vec![primary, secondary]);
let mut ctx = Context::new(Message::new());
fallback_plugin.execute(&mut ctx).await.unwrap();
assert!(ctx.response().is_some());
assert!(!ctx.response().unwrap().answers().is_empty());
}
#[tokio::test]
async fn test_fallback_on_empty_response() {
let primary = Arc::new(TestPlugin {
name: "primary".to_string(),
should_fail: false,
should_empty: true, });
let secondary = Arc::new(TestPlugin {
name: "secondary".to_string(),
should_fail: false,
should_empty: false,
});
let fallback_plugin = FallbackPlugin::new(vec![primary, secondary]);
let mut ctx = Context::new(Message::new());
fallback_plugin.execute(&mut ctx).await.unwrap();
assert!(ctx.response().is_some());
assert!(!ctx.response().unwrap().answers().is_empty());
}
#[tokio::test]
async fn test_fallback_error_only_mode() {
let primary = Arc::new(TestPlugin {
name: "primary".to_string(),
should_fail: false,
should_empty: true, });
let secondary = Arc::new(TestPlugin {
name: "secondary".to_string(),
should_fail: false,
should_empty: false,
});
let fallback_plugin = FallbackPlugin::new(vec![primary, secondary]).error_only(true);
let mut ctx = Context::new(Message::new());
fallback_plugin.execute(&mut ctx).await.unwrap();
}
#[test]
fn test_exec_plugin_quick_setup() {
let plugin =
<FallbackPlugin as ExecPlugin>::quick_setup("fallback", "primary,secondary").unwrap();
assert_eq!(plugin.name(), "fallback");
let plugin = <FallbackPlugin as ExecPlugin>::quick_setup("fallback", "upstream").unwrap();
assert_eq!(plugin.name(), "fallback");
let result = <FallbackPlugin as ExecPlugin>::quick_setup("invalid", "primary");
assert!(result.is_err());
let result = <FallbackPlugin as ExecPlugin>::quick_setup("fallback", "");
assert!(result.is_err());
let plugin =
<FallbackPlugin as ExecPlugin>::quick_setup("fallback", " primary , secondary ")
.unwrap();
assert_eq!(plugin.name(), "fallback");
}
}