Skip to content

Instantly share code, notes, and snippets.

@brandonbryant12
Created September 22, 2025 18:20
Show Gist options
  • Save brandonbryant12/28fd80f653e6bf94f06f551f390f41ad to your computer and use it in GitHub Desktop.
Save brandonbryant12/28fd80f653e6bf94f06f551f390f41ad to your computer and use it in GitHub Desktop.
Plan for Implementing Circuit Breaker Functionality in Backstage Backend Using Opossum
Overview
The goal is to add circuit breaker protection to each backend plugin in Backstage using the Opossum library (assuming “opaussum” is a typo for “opossum”). This will ensure that if a plugin’s API endpoints experience repeated failures (e.g., internal errors leading to 500+ status codes), the circuit opens, and subsequent requests to that plugin are rejected with a 503 (Service Unavailable) response to prevent cascading failures. When the circuit half-opens and succeeds, it closes again.
Key requirements:
• Per-plugin circuits: Each backend plugin operates on its own independent circuit breaker instance.
• Automatic configuration: The implementation uses Backstage’s service override mechanism, so no changes are needed in individual plugins. New plugins added via backend.add(...) will automatically inherit the circuit breaker without any additional code.
This approach treats the circuit breaker as a server-side mechanism for incoming requests to plugin endpoints (mounted under /api/). Opossum is primarily designed for client-side calls, but we can adapt it for server-side use by observing request outcomes (success/failure based on response status).
Assumptions and Dependencies
• Backstage is using the new backend system (via createBackend() from @backstage/backend-app-api).
• Install Opossum: yarn add opossum.
• Circuit breaker triggers on failures (e.g., response status >= 500; configurable).
• Fallback: Immediate 503 response when the circuit is open.
• Configurable Opossum options (e.g., error threshold, timeout, reset timeout) can be pulled from app-config.yaml under a section like backend.circuitBreaker.
• No impact on authentication or other built-in middleware (circuit breaker is added after them).
High-Level Implementation Steps
1 Create a Custom HttpRouterService Factory:
◦ Override the core httpRouter service (referenced via coreServices.httpRouter from @backstage/backend-plugin-api).
◦ This service is instantiated per plugin, ensuring each gets its own router and circuit breaker.
◦ Use the exported building blocks from @backstage/backend-defaults/httpRouter to recreate the default behavior, then insert a custom circuit breaker middleware.
2 Implement the Circuit Breaker Middleware:
◦ Define a middleware that integrates Opossum to monitor request outcomes.
◦ The middleware fires the circuit for each incoming request:
▪ If closed: Proceed with the request chain and observe the response (resolve on success, reject on failure).
▪ If open: Immediately respond with 503 without processing the request.
◦ Use response events (res.on('finish')) to determine success/failure post-processing.
3 Integrate into the Backend:
◦ Add the custom factory to your backend instance in packages/backend/src/index.ts.
◦ This applies globally to all plugins.
4 Testing and Monitoring:
◦ Test by simulating failures in a plugin (e.g., throw errors in handlers) and verify the circuit opens/closes.
◦ Add logging for circuit events (Opossum emits events like ‘open’, ‘close’).
Detailed Code Plan
In packages/backend/src/index.ts (or a separate file for factories):
import { createBackend } from '@backstage/backend-defaults';
import {
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import {
createAuthIntegrationRouter,
createCookieAuthRefreshMiddleware,
createCredentialsBarrier,
createLifecycleMiddleware,
createRateLimitMiddleware,
} from '@backstage/backend-defaults/httpRouter'; // Building blocks for default impl
import PromiseRouter from 'express-promise-router'; // Used internally by default
import CircuitBreaker from 'opossum'; // Opossum import
import { Handler } from 'express'; // Type for handlers
// Define configurable Opossum options (pull from config if needed)
const defaultCircuitOptions = {
timeout: 3000, // ms before timeout failure
errorThresholdPercentage: 50, // Open after 50% failures
resetTimeout: 30000, // ms in open state before half-open
volumeThreshold: 10, // Min requests to evaluate
};
// Function to create the circuit breaker middleware
function createCircuitBreakerMiddleware(pluginId: string, config: any) { // config for options
const options = { ...defaultCircuitOptions, name: `circuit-${pluginId}` }; // Per-plugin name for logging
const circuit = new CircuitBreaker(async (req: any, res: any, next: any) => {
return new Promise((resolve, reject) => {
// Hook into response events to observe outcome
const cleanup = () => {
res.removeListener('finish', onFinish);
res.removeListener('error', onError);
};
const onFinish = () => {
cleanup();
if (res.statusCode >= 500) { // Consider 5xx as failure; adjust as needed (e.g., >=400)
reject(new Error(`Request failed with status ${res.statusCode}`));
} else {
resolve();
}
};
const onError = (err: Error) => {
cleanup();
reject(err);
};
res.on('finish', onFinish);
res.on('error', onError);
next(); // Proceed to downstream handlers
});
}, options);
// Log circuit events (optional)
circuit.on('open', () => console.log(`Circuit opened for plugin: ${pluginId}`));
circuit.on('close', () => console.log(`Circuit closed for plugin: ${pluginId}`));
circuit.on('halfOpen', () => console.log(`Circuit half-open for plugin: ${pluginId}`));
// Middleware handler
return async (req: any, res: any, next: any) => {
try {
await circuit.fire(req, res, next);
} catch (err) {
if (circuit.opened) {
res.status(503).send('Service Unavailable');
} else {
next(err); // Pass other errors down
}
}
};
}
// Custom factory for httpRouter service
const customHttpRouterFactory = createServiceFactory({
service: coreServices.httpRouter,
deps: {
plugin: coreServices.pluginMetadata,
config: coreServices.rootConfig,
lifecycle: coreServices.lifecycle,
rootHttpRouter: coreServices.rootHttpRouter,
auth: coreServices.auth,
httpAuth: coreServices.httpAuth,
},
async factory(deps) {
const { plugin, config, lifecycle, rootHttpRouter, auth, httpAuth } = deps;
const router = PromiseRouter();
// Add default middlewares (replicate default impl)
router.use(createRateLimitMiddleware({ pluginId: plugin.getId(), config })); // Optional if rate limiting enabled
const credentialsBarrier = createCredentialsBarrier({ httpAuth, config });
router.use(createAuthIntegrationRouter({ auth }));
router.use(createLifecycleMiddleware({ lifecycle }));
router.use(credentialsBarrier.middleware);
router.use(createCookieAuthRefreshMiddleware({ auth, httpAuth }));
// Add our circuit breaker middleware here (after auth/lifecycle, before plugin handlers)
router.use(createCircuitBreakerMiddleware(plugin.getId(), config));
// Mount the plugin's router to the root (default behavior)
rootHttpRouter.use(router, { path: `/api/${plugin.getId()}` });
// Return the service interface
return {
use(handler: Handler) {
router.use(handler);
},
addAuthPolicy(policy) {
credentialsBarrier.addAuthPolicy(policy);
},
};
},
});
// Create and start backend with the custom factory
const backend = createBackend();
backend.add(customHttpRouterFactory); // Override the default httpRouter
// Add your plugins as usual, e.g., backend.add(import('@backstage/plugin-catalog-backend/alpha'));
backend.start();
Explanation of Key Parts
• Per-Plugin Isolation: The factory runs per plugin (via plugin.getId()), creating unique circuits and routers.
• Circuit Logic: The middleware observes response status without blocking the request chain. Failures update the circuit state asynchronously.
• Automatic for New Plugins: By overriding the core service, all plugins (existing and new) use the wrapped router without modification.
• Customization: Adjust defaultCircuitOptions or pull from config (e.g., config.getOptionalConfig('backend.circuitBreaker')). For plugin-specific options, use config.getOptionalConfig(plugins.${pluginId}.circuitBreaker).
• Fallback and Recovery: Opossum handles half-open state automatically. Add fallback in Opossum if needed (e.g., circuit.fallback(() => Promise.reject('Circuit open'))).
Potential Enhancements
• Configurable Thresholds: Make failure criteria configurable (e.g., include 4xx as failures).
• Metrics: Integrate with Backstage’s telemetry or Prometheus for circuit state monitoring.
• Error Handling: Exclude certain paths (e.g., health checks) from circuit counting via auth policies or conditional middleware.
• Testing: Use Opossum’s event emitters to assert states in integration tests.
This plan ensures resilience without requiring plugin authors to implement anything manually. If the intent was for outgoing requests instead, we could adapt by creating a custom http service factory with Opossum-wrapped fetch/axios, but that would require plugins to depend on and use that service explicitly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment