emailr_
Tous les articles
usecase·9 min

Failover email: Construire la redondance

reliabilityfailoverarchitecture

Email est une infrastructure critique. Quand votre fournisseur principal tombe en panne, vous avez besoin de failover. Voici comment construire des systèmes email redondants.

Pourquoi le failover est important

  • Les pannes des fournisseurs arrivent
  • Les limites de débit d'API peuvent bloquer l'envoi
  • Les problèmes DNS affectent la délivrabilité
  • Les incidents régionaux impactent la disponibilité

Architecture multi-fournisseurs

Abstraction des fournisseurs

interface EmailProvider {
  name: string;
  send(email: Email): Promise<SendResult>;
  checkHealth(): Promise<boolean>;
  getRateLimit(): Promise<RateLimitStatus>;
}

class EmailrProvider implements EmailProvider {
  name = 'emailr';
  
  async send(email: Email): Promise<SendResult> {
    return this.client.emails.send(email);
  }
  
  async checkHealth(): Promise<boolean> {
    try {
      await this.client.health.check();
      return true;
    } catch {
      return false;
    }
  }
}


class SendGridProvider implements EmailProvider {
  name = 'sendgrid';
  // ... implementation
}

Logique de failover

class EmailService {
  private providers: EmailProvider[];
  private primaryIndex = 0;
  
  constructor(providers: EmailProvider[]) {
    this.providers = providers;
  }
  
  async send(email: Email): Promise<SendResult> {
    const provider = this.providers[this.primaryIndex];
    
    try {
      if (!await provider.checkHealth()) {
        return this.failover(email);
      }
      
      return await provider.send(email);
    } catch (error) {
      return this.failover(email, error);
    }
  }
  
  private async failover(email: Email, error?: Error): Promise<SendResult> {
    for (let i = 0; i < this.providers.length; i++) {
      if (i === this.primaryIndex) continue;
      
      const provider = this.providers[i];
      
      try {
        if (await provider.checkHealth()) {
          const result = await provider.send(email);
          
          // Log failover event
          await logFailover(this.providers[this.primaryIndex].name, provider.name, error);
          
          return result;
        }
      } catch (e) {
        continue;
      }
    }
    
    throw new Error('All email providers failed');
  }
}

Contrôles d'état

Surveillance proactive de l'état

class HealthChecker {
  private healthStatus: Map<string, boolean> = new Map();
  
  async startMonitoring(providers: EmailProvider[], interval = 30000) {
    setInterval(async () => {
      for (const provider of providers) {
        const healthy = await provider.checkHealth();
        this.healthStatus.set(provider.name, healthy);
        
        if (!healthy) {
          await alertOps(`Provider ${provider.name} unhealthy`);
        }
      }
    }, interval);
  }
  
  isHealthy(providerName: string): boolean {
    return this.healthStatus.get(providerName) ?? true;
  }
}

Patron Circuit Breaker

class CircuitBreaker {
  private failures = 0;
  private lastFailure?: Date;
  private state: 'closed' | 'open' | 'half-open' = 'closed';
  
  private readonly threshold = 5;
  private readonly timeout = 60000; // 1 minute
  
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailure!.getTime() > this.timeout) {
        this.state = 'half-open';
      } else {
        throw new Error('Circuit breaker open');
      }
    }
    
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  private onSuccess() {
    this.failures = 0;
    this.state = 'closed';
  }
  
  private onFailure() {
    this.failures++;
    this.lastFailure = new Date();
    
    if (this.failures >= this.threshold) {
      this.state = 'open';
    }
  }
}

Équilibrage de charge

Distribution pondérée

class LoadBalancer {
  private providers: Array<{ provider: EmailProvider; weight: number }>;
  
  selectProvider(): EmailProvider {
    const totalWeight = this.providers.reduce((sum, p) => sum + p.weight, 0);
    let random = Math.random() * totalWeight;
    
    for (const { provider, weight } of this.providers) {
      random -= weight;
      if (random <= 0) {
        return provider;
      }
    }
    
    return this.providers[0].provider;
  }
}

// Usage: 70% primary, 30% secondary
const balancer = new LoadBalancer([
  { provider: emailr, weight: 70 },
  { provider: sendgrid, weight: 30 }
]);

Failover basé sur le DNS

Enregistrements MX multiples

; Primary provider
@ MX 10 mx1.emailr.dev.
@ MX 10 mx2.emailr.dev.

; Backup provider
@ MX 20 mx1.backup-provider.com.
@ MX 20 mx2.backup-provider.com.

Dégradation progressive

async function sendWithDegradation(email: Email): Promise<SendResult> {
  try {
    // Try full-featured send
    return await primaryProvider.send(email);
  } catch (error) {
    // Degrade to basic send (no tracking, simpler template)
    const degradedEmail = {
      ...email,
      html: stripTracking(email.html),
      trackOpens: false,
      trackClicks: false
    };
    
    return await backupProvider.send(degradedEmail);
  }
}

Bonnes pratiques

  1. Plusieurs fournisseurs - Au moins deux pour la redondance
  2. Health checks - Supervision proactive
  3. Circuit breakers - Éviter les pannes en cascade
  4. Dégradation progressive - Une fonctionnalité partielle vaut mieux que rien
  5. Alerte en cas de failover - Sachez quand cela arrive
  6. Test du failover - Exercices réguliers

Le failover email est une assurance. Vous espérez ne jamais en avoir besoin, mais le moment venu, il est inestimable.

e_

Écrit par l'équipe emailr

Nous construisons l'infrastructure email pour les développeurs

Prêt à commencer à envoyer ?

Obtenez votre clé API et envoyez votre premier email en moins de 5 minutes. Aucune carte de crédit requise.