Building uma Custom Plugin de retry

Este guia shows como para criar um plugin de retry personalizado com advanced features like circuit breaker patterns, componente de fallbacks, e intelligent tratamento de erros for Module Federation aplicações.

Visão geral

Quando building distributed aplicações com Module Federation, handling offline remotes gracefully é crucial for maintaining aplicação stability. Um plugin de retry personalizado pode forneça sophisticated tratamento de erros beyond basic retry mechanisms.

Core Features

Uma robust retry plugin deve include:

  • Retry Logic: Configurable retry attempts com exponential backoff
  • Circuit Breaker Pattern: Prevent cascading falhas by temporarily disabling falhou remotes
  • Fallback Componentes: Graceful UI degradation quando remotes são unavailable
  • Timeout Handling: Prevent hanging requests
  • Intelligent Estado Management: Track remote health e falha patterns

Implementation

Básico Plugin Structure

import type { ModuleFederationRuntimePlugin } from "@module-federation/enhanced/runtime";

interface RetryConfig {
  enableLogging?: boolean;
  fallbackTimeout?: number;
  retryAttempts?: number;
  retryDelay?: number;
  enableCircuitBreaker?: boolean;
  circuitBreakerThreshold?: number;
  circuitBreakerResetTimeout?: number;
}

const customRetryPlugin = (config: RetryConfig = {}): ModuleFederationRuntimePlugin => {
  const {
    enableLogging = true,
    fallbackTimeout = 5000,
    retryAttempts = 2,
    retryDelay = 1000,
    enableCircuitBreaker = true,
    circuitBreakerThreshold = 3,
    circuitBreakerResetTimeout = 60000,
  } = config;

  return {
    name: "custom-retry-plugin",
    // Implementation details below...
  };
};

Estado Management

Track remote health usando uma estado management system:

interface RemoteState {
  failureCount: number;
  lastFailureTime: number;
  isCircuitOpen: boolean;
}

// Track remote states for circuit breaker pattern
const remoteStates = new Map<string, RemoteState>();

const getRemoteState = (remoteId: string): RemoteState => {
  if (!remoteStates.has(remoteId)) {
    remoteStates.set(remoteId, {
      failureCount: 0,
      lastFailureTime: 0,
      isCircuitOpen: false,
    });
  }
  return remoteStates.get(remoteId)!;
};

const updateRemoteState = (remoteId: string, isSuccess: boolean) => {
  const state = getRemoteState(remoteId);
  
  if (isSuccess) {
    // Reset on success
    state.failureCount = 0;
    state.isCircuitOpen = false;
  } else {
    // Increment failure count
    state.failureCount++;
    state.lastFailureTime = Date.now();
    
    // Open circuit if threshold reached
    if (enableCircuitBreaker && state.failureCount >= circuitBreakerThreshold) {
      state.isCircuitOpen = true;
      
      // Auto-reset circuit after timeout
      setTimeout(() => {
        state.isCircuitOpen = false;
        state.failureCount = 0;
      }, circuitBreakerResetTimeout);
    }
  }
};

Retry Logic com Timeout

Implement exponential backoff com timeout protection:

const withRetry = async <T>(
  operation: () => Promise<T>,
  remoteId: string,
  attempts = retryAttempts
): Promise<T> => {
  let lastError: Error;
  
  for (let i = 0; i < attempts; i++) {
    try {
      const result = await Promise.race([
        operation(),
        new Promise<never>((_, reject) => 
          setTimeout(() => reject(new Error('Request timeout')), fallbackTimeout)
        )
      ]);
      
      // Success - update state
      updateRemoteState(remoteId, true);
      return result;
    } catch (error) {
      lastError = error as Error;
      
      // Wait before retry (exponential backoff)
      if (i < attempts - 1) {
        await new Promise(resolve => setTimeout(resolve, retryDelay * (i + 1)));
      }
    }
  }
  
  // All attempts failed
  updateRemoteState(remoteId, false);
  throw lastError!;
};

Erro Handling by Lifecycle

Handle diferente erro tipos based no falha stage:

async errorLoadRemote(args) {
  const { id, error, from, lifecycle } = args;
  const remoteId = id || from || 'unknown';

  // Different handling based on lifecycle
  switch (lifecycle) {
    case 'beforeRequest':
    case 'afterResolve':
      // Manifest loading failed
      const state = getRemoteState(remoteId);
      if (state.isCircuitOpen) {
        return createFallbackModule(remoteId, error);
      }
      
      try {
        // Retry the original request
        return await withRetry(() => args.origin(), remoteId);
      } catch (retryError) {
        return createFallbackModule(remoteId, error);
      }

    case 'onLoad':
      // Module loading failed after manifest was loaded
      return () => createFallbackModule(remoteId, error);

    case 'beforeLoadShare':
      // Shared dependency loading failed
      return createFallbackModule(remoteId, error);

    default:
      return createFallbackModule(remoteId, error);
  }
}

Fallback Componentes

Crie graceful fallback UI componentes:

const createFallbackComponent = (remoteId: string, error?: Error) => {
  const FallbackComponent = async () => {
    // Dynamically import React to avoid eager loading issues
    const React = await import('react');
    
    // Build error details safely
    const errorDetails = error ? [
      React.createElement("details", { key: "error" }, [
        React.createElement("summary", { key: "summary" }, "Error Details"),
        React.createElement("pre", { 
          key: "error-details",
          style: { 
            overflow: "auto",
            fontSize: "12px",
            textAlign: "left" as const
          }
        }, error?.message || String(error))
      ])
    ] : null;
    
    return React.createElement("div", {
      style: {
        padding: "16px",
        margin: "8px",
        border: "2px dashed #ffa39e",
        borderRadius: "8px",
        backgroundColor: "#fff2f0",
        color: "#cf1322",
        textAlign: "center" as const,
      },
    }, [
      React.createElement("h3", { key: "title" }, "Remote Module Unavailable"),
      React.createElement("p", { key: "description" }, 
        `The remote module "${remoteId}" is currently offline.`),
      errorDetails
    ].filter(Boolean));
  };

  FallbackComponent.displayName = `FallbackComponent_${remoteId}`;
  return FallbackComponent;
};

const createFallbackModule = (remoteId: string, error?: Error) => {
  try {
    const FallbackComponent = createFallbackComponent(remoteId, error);
    
    return {
      __esModule: true,
      default: FallbackComponent,
      [remoteId]: FallbackComponent,
    };
  } catch (createError) {
    // If fallback creation fails, return minimal module
    console.error('Failed to create fallback module:', createError);
    return {
      __esModule: true,
      default: () => null,
    };
  }
};

Uso

Configuração

import { init } from '@module-federation/enhanced/runtime';
import customRetryPlugin from './custom-retry-plugin';

init({
  name: 'host',
  remotes: [
    {
      name: 'remote1',
      entry: 'http://localhost:3001/remoteEntry.js'
    }
  ],
  plugins: [
    customRetryPlugin({
      enableLogging: true,
      retryAttempts: 3,
      retryDelay: 1000,
      fallbackTimeout: 5000,
      enableCircuitBreaker: true,
      circuitBreakerThreshold: 5,
      circuitBreakerResetTimeout: 30000,
    })
  ]
});

Integração com Compartilhe Estratégia

Be aware de como shareStrategy affects carregamento de remote:

// Version-first strategy eagerly loads remotes
// This can trigger failures during app startup
shareStrategy: 'version-first'

// Loaded-first strategy loads remotes on-demand
// Failures occur only when modules are actually requested
shareStrategy: 'loaded-first' // Recommended for offline scenarios

For aplicações esse precisa handle offline scenarios gracefully, consider usando loaded-first estratégia combined com your retry plugin.

Best Practices

  1. Graceful Degradation: Always forneça meaningful fallback UI
  2. Circuit Breaker: Prevent cascading falhas com circuit breaker pattern
  3. Logging: Include comprehensive logging for debugging
  4. Performance: Cache fallback módulos para avoid recreation
  5. Recovery: Allow automatic recovery quando remotes come back online
  6. Configuração: Make retry behavior configurable per environment

Complete Exemplo

Here's uma simplified versão de uma production-ready retry plugin:

import type { ModuleFederationRuntimePlugin } from "@module-federation/enhanced/runtime";
import type { ComponentType } from "react";

interface OfflineFallbackConfig {
  enableLogging?: boolean;
  fallbackTimeout?: number;
  retryAttempts?: number;
  retryDelay?: number;
  enableCircuitBreaker?: boolean;
  circuitBreakerThreshold?: number;
  circuitBreakerResetTimeout?: number;
  fallbackComponents?: Record<string, ComponentType>;
}

const enhancedOfflineFallbackPlugin = (
  config: OfflineFallbackConfig = {}
): ModuleFederationRuntimePlugin => {
  const {
    enableLogging = true,
    fallbackTimeout = 5000,
    retryAttempts = 2,
    retryDelay = 1000,
    enableCircuitBreaker = true,
    circuitBreakerThreshold = 3,
    circuitBreakerResetTimeout = 60000,
    fallbackComponents = {},
  } = config;

  const remoteStates = new Map<string, any>();
  const fallbackCache = new Map<string, ComponentType>();

  const log = (message: string, ...args: any[]) => {
    if (enableLogging) {
      console.warn(`[OfflineFallbackPlugin] ${message}`, ...args);
    }
  };

  const createFallbackComponent = (remoteId: string, error?: Error) => {
    if (fallbackComponents[remoteId]) {
      return fallbackComponents[remoteId];
    }

    const FallbackComponent = async () => {
      // Dynamically import React to avoid eager loading issues
      const React = await import('react');
      
      return React.createElement("div", {
        style: {
          padding: "16px",
          border: "2px dashed #ffa39e",
          borderRadius: "8px",
          backgroundColor: "#fff2f0",
          color: "#cf1322",
          textAlign: "center",
        },
      }, [
        React.createElement("h3", { key: "title" }, "Remote Module Unavailable"),
        React.createElement("p", { key: "description" }, 
          `The remote module "${remoteId}" is currently offline.`),
      ]);
    };

    return FallbackComponent;
  };

  return {
    name: "enhanced-offline-fallback-plugin",
    
    async errorLoadRemote(args) {
      const { id, error, lifecycle } = args;
      log(`Remote loading failed: ${id}`, { lifecycle, error: error?.message });

      switch (lifecycle) {
        case 'afterResolve':
          // Manifest loading failed
          return {
            id: 'fallback',
            name: 'fallback',
            metaData: {},
            shared: [],
            remotes: [],
            exposes: []
          };

        case 'onLoad':
          // Module loading failed
          return () => ({
            __esModule: true,
            default: createFallbackComponent(id, error),
          });

        default:
          return createFallbackComponent(id, error);
      }
    },

    onLoad(args) {
      log(`Successfully loaded remote: ${args.id}`);
      return args;
    },
  };
};

export default enhancedOfflineFallbackPlugin;

Uso em Rspack Configuração

import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack";
import enhancedOfflineFallbackPlugin from "./enhanced-offline-fallback-plugin";

export default defineConfig({
  plugins: [
    new ModuleFederationPlugin({
      name: "hostApp",
      shareStrategy: "loaded-first", // Recommended for offline handling
      remotes: {
        "remote-app": "remoteApp@http://localhost:8081/remote-mf-manifest.json",
      },
      runtimePlugins: [
        "./enhanced-offline-fallback-plugin.ts",
      ],
    }),
  ],
});

Avançado Features

Consider adding these advanced features:

  • Health Checks: Periodic health checks for circuit breaker recovery
  • Alternative Sources: Try carregamento de multiple manifest URLs
  • Metrics: Collect metrics on remote reliability
  • User Notifications: Notify users about offline remotes
  • Progressive Enhancement: Gracefully handle partial functionality

Este approach fornece robust tratamento de erros enquanto maintaining uma good user experience even quando módulos remotos são unavailable.