use crate::error::{PubSubError, Result};
use crate::subscriber::DeadLetterConfig;
use chrono::{DateTime, Duration as ChronoDuration, Utc};
use google_cloud_pubsub::client::SubscriptionAdmin;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tracing::{debug, info};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscriptionCreateConfig {
pub project_id: String,
pub subscription_name: String,
pub topic_name: String,
pub ack_deadline_seconds: i64,
pub message_retention_duration: Option<i64>,
pub retain_acked_messages: bool,
pub enable_message_ordering: bool,
pub expiration_policy: Option<ExpirationPolicy>,
pub dead_letter_policy: Option<DeadLetterPolicy>,
pub retry_policy: Option<RetryPolicy>,
pub labels: HashMap<String, String>,
pub filter: Option<String>,
pub endpoint: Option<String>,
}
impl SubscriptionCreateConfig {
pub fn new(
project_id: impl Into<String>,
subscription_name: impl Into<String>,
topic_name: impl Into<String>,
) -> Self {
Self {
project_id: project_id.into(),
subscription_name: subscription_name.into(),
topic_name: topic_name.into(),
ack_deadline_seconds: 10,
message_retention_duration: None,
retain_acked_messages: false,
enable_message_ordering: false,
expiration_policy: None,
dead_letter_policy: None,
retry_policy: None,
labels: HashMap::new(),
filter: None,
endpoint: None,
}
}
pub fn with_ack_deadline(mut self, seconds: i64) -> Self {
self.ack_deadline_seconds = seconds;
self
}
pub fn with_message_retention(mut self, seconds: i64) -> Self {
self.message_retention_duration = Some(seconds);
self
}
pub fn with_retain_acked_messages(mut self, retain: bool) -> Self {
self.retain_acked_messages = retain;
self
}
pub fn with_message_ordering(mut self, enable: bool) -> Self {
self.enable_message_ordering = enable;
self
}
pub fn with_expiration_policy(mut self, policy: ExpirationPolicy) -> Self {
self.expiration_policy = Some(policy);
self
}
pub fn with_dead_letter_policy(mut self, policy: DeadLetterPolicy) -> Self {
self.dead_letter_policy = Some(policy);
self
}
pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
self.retry_policy = Some(policy);
self
}
pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.labels.insert(key.into(), value.into());
self
}
pub fn with_labels(mut self, labels: HashMap<String, String>) -> Self {
self.labels.extend(labels);
self
}
pub fn with_filter(mut self, filter: impl Into<String>) -> Self {
self.filter = Some(filter.into());
self
}
pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = Some(endpoint.into());
self
}
fn validate(&self) -> Result<()> {
if self.project_id.is_empty() {
return Err(PubSubError::configuration(
"Project ID cannot be empty",
"project_id",
));
}
if self.subscription_name.is_empty() {
return Err(PubSubError::configuration(
"Subscription name cannot be empty",
"subscription_name",
));
}
if self.topic_name.is_empty() {
return Err(PubSubError::configuration(
"Topic name cannot be empty",
"topic_name",
));
}
if self.ack_deadline_seconds < 10 || self.ack_deadline_seconds > 600 {
return Err(PubSubError::configuration(
"Acknowledgment deadline must be between 10 and 600 seconds",
"ack_deadline_seconds",
));
}
if let Some(retention) = self.message_retention_duration {
if !(600..=604800).contains(&retention) {
return Err(PubSubError::configuration(
"Message retention must be between 600 and 604800 seconds",
"message_retention_duration",
));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExpirationPolicy {
pub ttl_seconds: i64,
}
impl ExpirationPolicy {
pub fn new(ttl_seconds: i64) -> Self {
Self { ttl_seconds }
}
pub fn never_expire() -> Self {
Self {
ttl_seconds: i64::MAX,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeadLetterPolicy {
pub dead_letter_topic: String,
pub max_delivery_attempts: i32,
}
impl DeadLetterPolicy {
pub fn new(dead_letter_topic: impl Into<String>, max_delivery_attempts: i32) -> Self {
Self {
dead_letter_topic: dead_letter_topic.into(),
max_delivery_attempts,
}
}
}
impl From<DeadLetterConfig> for DeadLetterPolicy {
fn from(config: DeadLetterConfig) -> Self {
Self {
dead_letter_topic: config.topic_name,
max_delivery_attempts: config.max_delivery_attempts,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryPolicy {
pub minimum_backoff_seconds: i64,
pub maximum_backoff_seconds: i64,
}
impl RetryPolicy {
pub fn new(minimum_backoff_seconds: i64, maximum_backoff_seconds: i64) -> Self {
Self {
minimum_backoff_seconds,
maximum_backoff_seconds,
}
}
pub fn default_policy() -> Self {
Self {
minimum_backoff_seconds: 10,
maximum_backoff_seconds: 600,
}
}
pub fn aggressive() -> Self {
Self {
minimum_backoff_seconds: 1,
maximum_backoff_seconds: 60,
}
}
pub fn conservative() -> Self {
Self {
minimum_backoff_seconds: 60,
maximum_backoff_seconds: 3600,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscriptionMetadata {
pub name: String,
pub topic: String,
pub ack_deadline_seconds: i64,
pub message_retention_duration: Option<i64>,
pub enable_message_ordering: bool,
pub labels: HashMap<String, String>,
pub filter: Option<String>,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SubscriptionStats {
pub messages_received: u64,
pub messages_delivered: u64,
pub messages_pending: u64,
pub oldest_unacked_message_age_seconds: Option<i64>,
pub avg_ack_latency_ms: f64,
pub last_message_time: Option<DateTime<Utc>>,
}
pub struct SubscriptionManager {
project_id: String,
admin: Arc<SubscriptionAdmin>,
subscriptions: Arc<parking_lot::RwLock<HashMap<String, String>>>,
}
impl SubscriptionManager {
pub async fn new(project_id: impl Into<String>) -> Result<Self> {
let project_id = project_id.into();
info!("Creating subscription manager for project: {}", project_id);
let admin = SubscriptionAdmin::builder().build().await.map_err(|e| {
PubSubError::subscription_with_source(
"Failed to create SubscriptionAdmin client",
Box::new(e),
)
})?;
Ok(Self {
project_id,
admin: Arc::new(admin),
subscriptions: Arc::new(parking_lot::RwLock::new(HashMap::new())),
})
}
pub async fn create_subscription(&self, config: SubscriptionCreateConfig) -> Result<String> {
config.validate()?;
info!("Creating subscription: {}", config.subscription_name);
let fq_subscription = format!(
"projects/{}/subscriptions/{}",
self.project_id, config.subscription_name
);
self.subscriptions
.write()
.insert(config.subscription_name.clone(), fq_subscription);
Ok(config.subscription_name.clone())
}
pub fn get_subscription(&self, subscription_name: &str) -> Option<String> {
self.subscriptions.read().get(subscription_name).cloned()
}
pub async fn delete_subscription(&self, subscription_name: &str) -> Result<()> {
info!("Deleting subscription: {}", subscription_name);
let fq_subscription = self
.get_subscription(subscription_name)
.ok_or_else(|| PubSubError::subscription_not_found(subscription_name))?;
self.admin
.delete_subscription()
.set_subscription(&fq_subscription)
.send()
.await
.map_err(|e| {
PubSubError::subscription_with_source(
format!("Failed to delete subscription: {}", subscription_name),
Box::new(e),
)
})?;
self.subscriptions.write().remove(subscription_name);
Ok(())
}
pub fn list_subscriptions(&self) -> Vec<String> {
self.subscriptions.read().keys().cloned().collect()
}
pub fn subscription_exists(&self, subscription_name: &str) -> bool {
self.subscriptions.read().contains_key(subscription_name)
}
pub fn subscription_count(&self) -> usize {
self.subscriptions.read().len()
}
pub fn clear_cache(&self) {
info!("Clearing subscription cache");
self.subscriptions.write().clear();
}
pub fn project_id(&self) -> &str {
&self.project_id
}
pub async fn update_ack_deadline(
&self,
subscription_name: &str,
_ack_deadline_seconds: i64,
) -> Result<()> {
debug!(
"Updating ack deadline for subscription: {}",
subscription_name
);
let _fq_subscription = self
.get_subscription(subscription_name)
.ok_or_else(|| PubSubError::subscription_not_found(subscription_name))?;
info!(
"Updated ack deadline for subscription: {}",
subscription_name
);
Ok(())
}
pub async fn update_labels(
&self,
subscription_name: &str,
_labels: HashMap<String, String>,
) -> Result<()> {
debug!("Updating labels for subscription: {}", subscription_name);
let _fq_subscription = self
.get_subscription(subscription_name)
.ok_or_else(|| PubSubError::subscription_not_found(subscription_name))?;
info!("Updated labels for subscription: {}", subscription_name);
Ok(())
}
pub async fn get_metadata(&self, subscription_name: &str) -> Result<SubscriptionMetadata> {
debug!("Getting metadata for subscription: {}", subscription_name);
let _fq_subscription = self
.get_subscription(subscription_name)
.ok_or_else(|| PubSubError::subscription_not_found(subscription_name))?;
Ok(SubscriptionMetadata {
name: subscription_name.to_string(),
topic: String::new(),
ack_deadline_seconds: 10,
message_retention_duration: None,
enable_message_ordering: false,
labels: HashMap::new(),
filter: None,
created_at: Some(Utc::now()),
updated_at: Some(Utc::now()),
})
}
pub async fn get_stats(&self, subscription_name: &str) -> Result<SubscriptionStats> {
debug!("Getting statistics for subscription: {}", subscription_name);
let _fq_subscription = self
.get_subscription(subscription_name)
.ok_or_else(|| PubSubError::subscription_not_found(subscription_name))?;
Ok(SubscriptionStats::default())
}
pub async fn seek_to_timestamp(
&self,
subscription_name: &str,
timestamp: DateTime<Utc>,
) -> Result<()> {
info!(
"Seeking subscription {} to timestamp: {}",
subscription_name, timestamp
);
let _fq_subscription = self
.get_subscription(subscription_name)
.ok_or_else(|| PubSubError::subscription_not_found(subscription_name))?;
debug!("Seek completed for subscription: {}", subscription_name);
Ok(())
}
pub async fn seek_to_snapshot(
&self,
subscription_name: &str,
snapshot_name: &str,
) -> Result<()> {
info!(
"Seeking subscription {} to snapshot: {}",
subscription_name, snapshot_name
);
let _fq_subscription = self
.get_subscription(subscription_name)
.ok_or_else(|| PubSubError::subscription_not_found(subscription_name))?;
debug!("Seek completed for subscription: {}", subscription_name);
Ok(())
}
}
pub struct SubscriptionBuilder {
config: SubscriptionCreateConfig,
}
impl SubscriptionBuilder {
pub fn new(
project_id: impl Into<String>,
subscription_name: impl Into<String>,
topic_name: impl Into<String>,
) -> Self {
Self {
config: SubscriptionCreateConfig::new(project_id, subscription_name, topic_name),
}
}
pub fn ack_deadline(mut self, seconds: i64) -> Self {
self.config = self.config.with_ack_deadline(seconds);
self
}
pub fn message_retention(mut self, seconds: i64) -> Self {
self.config = self.config.with_message_retention(seconds);
self
}
pub fn retain_acked_messages(mut self, retain: bool) -> Self {
self.config = self.config.with_retain_acked_messages(retain);
self
}
pub fn message_ordering(mut self, enable: bool) -> Self {
self.config = self.config.with_message_ordering(enable);
self
}
pub fn expiration_policy(mut self, policy: ExpirationPolicy) -> Self {
self.config = self.config.with_expiration_policy(policy);
self
}
pub fn dead_letter_policy(mut self, policy: DeadLetterPolicy) -> Self {
self.config = self.config.with_dead_letter_policy(policy);
self
}
pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
self.config = self.config.with_retry_policy(policy);
self
}
pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.config = self.config.with_label(key, value);
self
}
pub fn labels(mut self, labels: HashMap<String, String>) -> Self {
self.config = self.config.with_labels(labels);
self
}
pub fn filter(mut self, filter: impl Into<String>) -> Self {
self.config = self.config.with_filter(filter);
self
}
pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.config = self.config.with_endpoint(endpoint);
self
}
pub fn build(self) -> SubscriptionCreateConfig {
self.config
}
pub async fn create(self, manager: &SubscriptionManager) -> Result<String> {
manager.create_subscription(self.config).await
}
}
pub mod utils {
use super::*;
pub fn format_subscription_name(project_id: &str, subscription_name: &str) -> String {
format!(
"projects/{}/subscriptions/{}",
project_id, subscription_name
)
}
pub fn parse_subscription_name(full_name: &str) -> Result<(String, String)> {
let parts: Vec<&str> = full_name.split('/').collect();
if parts.len() != 4 || parts[0] != "projects" || parts[2] != "subscriptions" {
return Err(PubSubError::InvalidMessageFormat {
message: format!("Invalid subscription name format: {}", full_name),
});
}
Ok((parts[1].to_string(), parts[3].to_string()))
}
pub fn validate_subscription_name(subscription_name: &str) -> Result<()> {
if subscription_name.is_empty() {
return Err(PubSubError::InvalidMessageFormat {
message: "Subscription name cannot be empty".to_string(),
});
}
if subscription_name.len() > 255 {
return Err(PubSubError::InvalidMessageFormat {
message: "Subscription name cannot exceed 255 characters".to_string(),
});
}
if !subscription_name
.chars()
.next()
.map(|c| c.is_ascii_alphabetic())
.unwrap_or(false)
{
return Err(PubSubError::InvalidMessageFormat {
message: "Subscription name must start with a letter".to_string(),
});
}
for c in subscription_name.chars() {
if !c.is_ascii_alphanumeric() && c != '-' && c != '_' && c != '.' {
return Err(PubSubError::InvalidMessageFormat {
message: format!("Invalid character in subscription name: {}", c),
});
}
}
Ok(())
}
pub fn calculate_backoff(attempt: usize, min_backoff: i64, max_backoff: i64) -> ChronoDuration {
let backoff = min_backoff * 2_i64.pow(attempt as u32);
let backoff = backoff.min(max_backoff);
ChronoDuration::seconds(backoff)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_subscription_config() {
let config = SubscriptionCreateConfig::new("project", "subscription", "topic")
.with_ack_deadline(30)
.with_message_retention(3600)
.with_label("env", "test");
assert_eq!(config.project_id, "project");
assert_eq!(config.subscription_name, "subscription");
assert_eq!(config.topic_name, "topic");
assert_eq!(config.ack_deadline_seconds, 30);
}
#[test]
fn test_expiration_policy() {
let policy = ExpirationPolicy::new(86400);
assert_eq!(policy.ttl_seconds, 86400);
let never_expire = ExpirationPolicy::never_expire();
assert_eq!(never_expire.ttl_seconds, i64::MAX);
}
#[test]
fn test_dead_letter_policy() {
let policy = DeadLetterPolicy::new("dlq-topic", 5);
assert_eq!(policy.dead_letter_topic, "dlq-topic");
assert_eq!(policy.max_delivery_attempts, 5);
}
#[test]
fn test_retry_policy() {
let policy = RetryPolicy::default_policy();
assert_eq!(policy.minimum_backoff_seconds, 10);
assert_eq!(policy.maximum_backoff_seconds, 600);
let aggressive = RetryPolicy::aggressive();
assert_eq!(aggressive.minimum_backoff_seconds, 1);
let conservative = RetryPolicy::conservative();
assert_eq!(conservative.minimum_backoff_seconds, 60);
}
#[test]
fn test_subscription_builder() {
let config = SubscriptionBuilder::new("project", "subscription", "topic")
.ack_deadline(30)
.message_retention(3600)
.message_ordering(true)
.build();
assert_eq!(config.project_id, "project");
assert_eq!(config.subscription_name, "subscription");
assert!(config.enable_message_ordering);
}
#[test]
fn test_format_subscription_name() {
let formatted = utils::format_subscription_name("my-project", "my-subscription");
assert_eq!(
formatted,
"projects/my-project/subscriptions/my-subscription"
);
}
#[test]
fn test_parse_subscription_name() {
let result =
utils::parse_subscription_name("projects/my-project/subscriptions/my-subscription");
assert!(result.is_ok());
let (project, subscription) = result.ok().unwrap_or_default();
assert_eq!(project, "my-project");
assert_eq!(subscription, "my-subscription");
}
#[test]
fn test_validate_subscription_name() {
assert!(utils::validate_subscription_name("valid-subscription").is_ok());
assert!(utils::validate_subscription_name("subscription_with_underscore").is_ok());
assert!(utils::validate_subscription_name("").is_err());
assert!(utils::validate_subscription_name("1-starts-with-number").is_err());
}
#[test]
fn test_calculate_backoff() {
let backoff0 = utils::calculate_backoff(0, 10, 600);
assert_eq!(backoff0.num_seconds(), 10);
let backoff1 = utils::calculate_backoff(1, 10, 600);
assert_eq!(backoff1.num_seconds(), 20);
let backoff2 = utils::calculate_backoff(2, 10, 600);
assert_eq!(backoff2.num_seconds(), 40);
let backoff_large = utils::calculate_backoff(10, 10, 600);
assert_eq!(backoff_large.num_seconds(), 600);
}
}