use std::collections::{HashMap, HashSet};
use std::time::Instant;
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct SessionState {
prepared_statements: HashMap<String, String>,
listen_channels: HashSet<String>,
temporary_tables: HashSet<String>,
custom_gucs: HashMap<String, String>,
reconnect_init_sql: Option<String>,
in_transaction: bool,
}
impl SessionState {
pub fn new() -> Self {
Self::default()
}
pub fn has_state(&self) -> bool {
!self.prepared_statements.is_empty()
|| !self.listen_channels.is_empty()
|| !self.temporary_tables.is_empty()
|| !self.custom_gucs.is_empty()
}
pub fn is_reconnect_safe(&self) -> bool {
!self.in_transaction && !self.has_state()
}
pub fn track_prepare(&mut self, name: &str, sql: &str) {
self.prepared_statements
.insert(name.to_string(), sql.to_string());
}
pub fn track_close_statement(&mut self, name: &str) {
self.prepared_statements.remove(name);
}
pub fn get_statement_sql(&self, name: &str) -> Option<&str> {
self.prepared_statements.get(name).map(|s| s.as_str())
}
pub fn prepared_statement_names(&self) -> impl Iterator<Item = &str> {
self.prepared_statements.keys().map(|s| s.as_str())
}
pub fn prepared_statements(&self) -> &HashMap<String, String> {
&self.prepared_statements
}
pub fn track_listen(&mut self, channel: &str) {
self.listen_channels.insert(channel.to_string());
}
pub fn track_unlisten(&mut self, channel: &str) {
self.listen_channels.remove(channel);
}
pub fn listen_channels(&self) -> &HashSet<String> {
&self.listen_channels
}
pub fn clear_listen_channels(&mut self) {
self.listen_channels.clear();
}
pub fn track_temp_table(&mut self, name: &str) {
self.temporary_tables.insert(name.to_string());
}
pub fn temporary_tables(&self) -> &HashSet<String> {
&self.temporary_tables
}
pub fn track_set_guc(&mut self, key: &str, value: &str) {
self.custom_gucs.insert(key.to_string(), value.to_string());
}
pub fn custom_gucs(&self) -> &HashMap<String, String> {
&self.custom_gucs
}
pub fn set_reconnect_init_sql(&mut self, sql: impl Into<String>) {
self.reconnect_init_sql = Some(sql.into());
}
pub fn clear_reconnect_init_sql(&mut self) {
self.reconnect_init_sql = None;
}
pub fn reconnect_init_sql(&self) -> Option<&str> {
self.reconnect_init_sql.as_deref()
}
pub fn set_in_transaction(&mut self, in_transaction: bool) {
self.in_transaction = in_transaction;
}
pub fn in_transaction(&self) -> bool {
self.in_transaction
}
pub fn clear(&mut self) {
self.prepared_statements.clear();
self.listen_channels.clear();
self.temporary_tables.clear();
self.custom_gucs.clear();
self.in_transaction = false;
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct ConnectionHealth {
alive: bool,
reconnect_count: u32,
last_confirmed_alive: Option<Instant>,
needs_recovery: bool,
}
impl Default for ConnectionHealth {
fn default() -> Self {
Self::new()
}
}
impl ConnectionHealth {
pub fn new() -> Self {
Self {
alive: true,
reconnect_count: 0,
last_confirmed_alive: Some(Instant::now()),
needs_recovery: false,
}
}
pub fn is_alive(&self) -> bool {
self.alive
}
pub fn mark_alive(&mut self) {
self.alive = true;
self.last_confirmed_alive = Some(Instant::now());
}
pub fn mark_broken(&mut self) {
self.alive = false;
}
pub fn reconnect_count(&self) -> u32 {
self.reconnect_count
}
pub fn increment_reconnect_count(&mut self) {
self.reconnect_count += 1;
}
pub fn last_confirmed_alive(&self) -> Option<Instant> {
self.last_confirmed_alive
}
pub fn needs_recovery(&self) -> bool {
self.needs_recovery
}
pub fn set_needs_recovery(&mut self, needs_recovery: bool) {
self.needs_recovery = needs_recovery;
}
pub fn reset_after_reconnect(&mut self) {
self.alive = true;
self.reconnect_count += 1;
self.last_confirmed_alive = Some(Instant::now());
self.needs_recovery = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_session_state_empty() {
let state = SessionState::new();
assert!(!state.has_state());
assert!(state.is_reconnect_safe());
}
#[test]
fn test_session_state_with_prepared_statement() {
let mut state = SessionState::new();
state.track_prepare("stmt1", "SELECT 1");
assert!(state.has_state());
assert!(!state.is_reconnect_safe());
assert_eq!(state.get_statement_sql("stmt1"), Some("SELECT 1"));
}
#[test]
fn test_session_state_with_listen_channel() {
let mut state = SessionState::new();
state.track_listen("events");
assert!(state.has_state());
assert!(!state.is_reconnect_safe());
assert!(state.listen_channels().contains("events"));
}
#[test]
fn test_session_state_with_temp_table() {
let mut state = SessionState::new();
state.track_temp_table("tmp_data");
assert!(state.has_state());
assert!(!state.is_reconnect_safe());
}
#[test]
fn test_session_state_with_guc() {
let mut state = SessionState::new();
state.track_set_guc("timezone", "UTC");
assert!(state.has_state());
assert!(!state.is_reconnect_safe());
assert_eq!(
state.custom_gucs().get("timezone"),
Some(&"UTC".to_string())
);
}
#[test]
fn test_session_state_in_transaction() {
let mut state = SessionState::new();
state.set_in_transaction(true);
assert!(!state.is_reconnect_safe());
assert!(!state.has_state()); }
#[test]
fn test_session_state_unlisten() {
let mut state = SessionState::new();
state.track_listen("events");
assert!(state.has_state());
state.track_unlisten("events");
assert!(!state.has_state());
}
#[test]
fn test_session_state_clear_listen_channels() {
let mut state = SessionState::new();
state.track_listen("events");
state.track_listen("jobs");
assert!(state.has_state());
state.clear_listen_channels();
assert!(state.listen_channels().is_empty());
assert!(!state.has_state());
}
#[test]
fn test_session_state_close_statement() {
let mut state = SessionState::new();
state.track_prepare("stmt1", "SELECT 1");
assert!(state.has_state());
state.track_close_statement("stmt1");
assert!(!state.has_state());
}
#[test]
fn test_session_state_clear() {
let mut state = SessionState::new();
state.track_prepare("stmt1", "SELECT 1");
state.track_listen("events");
state.track_temp_table("tmp");
state.track_set_guc("timezone", "UTC");
state.set_in_transaction(true);
state.clear();
assert!(!state.has_state());
assert!(state.is_reconnect_safe());
}
#[test]
fn test_session_state_reconnect_init_sql() {
let mut state = SessionState::new();
assert_eq!(state.reconnect_init_sql(), None);
state.set_reconnect_init_sql("SET timezone = 'UTC'");
assert_eq!(state.reconnect_init_sql(), Some("SET timezone = 'UTC'"));
assert!(!state.has_state());
assert!(state.is_reconnect_safe());
state.clear_reconnect_init_sql();
assert_eq!(state.reconnect_init_sql(), None);
}
#[test]
fn test_connection_health_new() {
let health = ConnectionHealth::new();
assert!(health.is_alive());
assert_eq!(health.reconnect_count(), 0);
assert!(health.last_confirmed_alive().is_some());
assert!(!health.needs_recovery());
}
#[test]
fn test_connection_health_mark_broken() {
let mut health = ConnectionHealth::new();
health.mark_broken();
assert!(!health.is_alive());
}
#[test]
fn test_connection_health_mark_alive() {
let mut health = ConnectionHealth::new();
health.mark_broken();
health.mark_alive();
assert!(health.is_alive());
}
#[test]
fn test_connection_health_reset_after_reconnect() {
let mut health = ConnectionHealth::new();
health.mark_broken();
health.set_needs_recovery(true);
health.reset_after_reconnect();
assert!(health.is_alive());
assert_eq!(health.reconnect_count(), 1);
assert!(!health.needs_recovery());
}
}