Skip to main content

linkrs_query/optimizer/stats/feedback/
trigger.rs

1//! Automatic feedback triggering module
2//!
3//! Refer to PostgreSQL’s ANALYZE automatic trigger mechanism.
4//! Configure when statistical information should be automatically updated and when the selection criteria should be re-evaluated.
5
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::Instant;
8
9/// Automatic feedback triggering configuration
10///
11/// Configure when statistical updates should be triggered automatically, including the minimum number of samples, the error threshold, and the cooling period.
12///
13/// # Example
14/// ```
15/// use linkrs_query::optimizer::stats::feedback::trigger::AutoFeedbackConfig;
16///
17/// let config = AutoFeedbackConfig::new();
18/// assert!(config.enabled);
19/// assert_eq!(config.min_samples_for_update, 10);
20/// ```
21#[derive(Debug, Clone)]
22pub struct AutoFeedbackConfig {
23    /// The minimum number of feedback samples triggers a re-evaluation.
24    pub min_samples_for_update: usize,
25    /// The error threshold triggers an emergency update (the update is performed immediately when the error exceeds this value).
26    pub error_threshold: f64,
27    /// Update the cooling time (to avoid frequent updates).
28    pub update_cooldown_ms: u64,
29    /// Maximum number of feedback records in history
30    pub max_feedback_history: usize,
31    /// Should automatic updates be enabled?
32    pub enabled: bool,
33}
34
35impl AutoFeedbackConfig {
36    /// Create the default configuration.
37    pub fn new() -> Self {
38        Self {
39            min_samples_for_update: 10,
40            error_threshold: 0.5,
41            update_cooldown_ms: 60000, // 1 minute
42            max_feedback_history: 100,
43            enabled: true,
44        }
45    }
46
47    /// Create using custom parameters
48    pub fn with_params(
49        min_samples: usize,
50        error_threshold: f64,
51        cooldown_ms: u64,
52        max_history: usize,
53    ) -> Self {
54        Self {
55            min_samples_for_update: min_samples,
56            error_threshold: error_threshold.clamp(0.1, 1.0),
57            update_cooldown_ms: cooldown_ms,
58            max_feedback_history: max_history,
59            enabled: true,
60        }
61    }
62
63    /// Check whether an update should be triggered.
64    ///
65    /// # Parameters
66    /// `feedback_count`: The current number of feedback samples.
67    /// `last_update_ms`: Time of the last update (in milliseconds as a timestamp)
68    /// `current_error`: The current estimated error
69    ///
70    /// # Return
71    /// `true`: The update should be triggered.
72    /// `false`: No update is required.
73    pub fn should_trigger_update(
74        &self,
75        feedback_count: usize,
76        last_update_ms: u64,
77        current_error: f64,
78    ) -> bool {
79        if !self.enabled {
80            return false;
81        }
82
83        // Check the error threshold (urgent update).
84        if current_error > self.error_threshold {
85            return true;
86        }
87
88        // Check the minimum sample size.
89        if feedback_count < self.min_samples_for_update {
90            return false;
91        }
92
93        // Check the cooling time.
94        let current_time = Instant::now().elapsed().as_millis() as u64;
95        if current_time.saturating_sub(last_update_ms) < self.update_cooldown_ms {
96            return false;
97        }
98
99        true
100    }
101
102    /// Enable automatic updates
103    pub fn enable(&mut self) {
104        self.enabled = true;
105    }
106
107    /// Disable automatic updates.
108    pub fn disable(&mut self) {
109        self.enabled = false;
110    }
111}
112
113impl Default for AutoFeedbackConfig {
114    fn default() -> Self {
115        Self::new()
116    }
117}
118
119/// Automatic feedback trigger
120///
121/// Automatically determines whether to trigger a statistical update based on the configuration.
122/// Use atomic operations to ensure thread safety.
123///
124/// # Examples
125/// ```
126/// use linkrs_query::optimizer::stats::feedback::trigger::{AutoFeedbackTrigger, AutoFeedbackConfig};
127///
128/// let config = AutoFeedbackConfig::with_params(5, 0.5, 1000, 50);
129/// let trigger = AutoFeedbackTrigger::new(config);
130///
131/// // Record feedback
132/// for _ in 0..5 {
133///     trigger.record_feedback();
134/// }
135///
136/// // Check whether an action should be triggered (if the error exceeds the threshold)
137/// assert!(trigger.should_trigger(0.6));
138/// ```
139#[derive(Debug)]
140pub struct AutoFeedbackTrigger {
141    /// Configuration
142    config: AutoFeedbackConfig,
143    /// Last update time
144    last_update_time_ms: AtomicU64,
145    /// Current feedback count
146    feedback_count: AtomicU64,
147}
148
149impl AutoFeedbackTrigger {
150    /// Create a new trigger.
151    pub fn new(config: AutoFeedbackConfig) -> Self {
152        Self {
153            config,
154            last_update_time_ms: AtomicU64::new(0),
155            feedback_count: AtomicU64::new(0),
156        }
157    }
158
159    /// Record feedback
160    pub fn record_feedback(&self) {
161        self.feedback_count.fetch_add(1, Ordering::Relaxed);
162    }
163
164    /// Check if an update should be triggered
165    pub fn should_trigger(&self, current_error: f64) -> bool {
166        let count = self.feedback_count.load(Ordering::Relaxed) as usize;
167        let last_update = self.last_update_time_ms.load(Ordering::Relaxed);
168        self.config
169            .should_trigger_update(count, last_update, current_error)
170    }
171
172    /// The marking update has been completed.
173    pub fn mark_updated(&self) {
174        let current_time = Instant::now().elapsed().as_millis() as u64;
175        self.last_update_time_ms
176            .store(current_time, Ordering::Relaxed);
177        self.feedback_count.store(0, Ordering::Relaxed);
178    }
179
180    /// Obtain the current count of feedback messages.
181    pub fn get_feedback_count(&self) -> u64 {
182        self.feedback_count.load(Ordering::Relaxed)
183    }
184
185    /// Update the configuration.
186    pub fn update_config(&mut self, config: AutoFeedbackConfig) {
187        self.config = config;
188    }
189
190    /// Obtain the configuration.
191    pub fn config(&self) -> &AutoFeedbackConfig {
192        &self.config
193    }
194}
195
196impl Default for AutoFeedbackTrigger {
197    fn default() -> Self {
198        Self::new(AutoFeedbackConfig::default())
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn test_auto_feedback_config() {
208        let config = AutoFeedbackConfig::new();
209        assert!(config.enabled);
210        assert_eq!(config.min_samples_for_update, 10);
211
212        // Test trigger logic
213        assert!(!config.should_trigger_update(5, 0, 0.1)); // Insufficient sample size
214        assert!(config.should_trigger_update(5, 0, 0.6)); // The error exceeds the threshold (urgent update).
215
216        // Test the normal triggering logic: If the program has been running for longer than the cooling period…
217        let current_time = Instant::now().elapsed().as_millis() as u64;
218        if current_time > config.update_cooldown_ms {
219            assert!(config.should_trigger_update(15, 0, 0.1)); // The sample is sufficient, and the cooling time has already passed.
220        }
221    }
222
223    #[test]
224    fn test_auto_feedback_trigger() {
225        let config = AutoFeedbackConfig::with_params(5, 0.5, 1000, 50);
226        let trigger = AutoFeedbackTrigger::new(config);
227
228        assert!(!trigger.should_trigger(0.1)); // No feedback.
229
230        // Record feedback
231        for _ in 0..5 {
232            trigger.record_feedback();
233        }
234
235        // An error exceeding the threshold should trigger a response.
236        assert!(trigger.should_trigger(0.6));
237
238        trigger.mark_updated();
239        assert_eq!(trigger.get_feedback_count(), 0);
240    }
241
242    #[test]
243    fn test_config_enable_disable() {
244        let mut config = AutoFeedbackConfig::new();
245        assert!(config.enabled);
246
247        config.disable();
248        assert!(!config.enabled);
249        assert!(!config.should_trigger_update(100, 0, 1.0));
250
251        config.enable();
252        assert!(config.enabled);
253    }
254}