polly_dart 0.0.6
polly_dart: ^0.0.6 copied to clipboard
A Dart port of Polly (.NET resilience library) providing strategies like Retry, Circuit Breaker, Timeout, Rate Limiter, Hedging, and Fallback.
Polly Dart #
A Dart port of Polly, the .NET resilience and transient-fault-handling library. Polly Dart allows developers to express resilience strategies such as Retry, Circuit Breaker, Timeout, Rate Limiter, Hedging, and Fallback in a fluent and thread-safe manner.
Documentation #
📚 Complete Documentation - Comprehensive guides, API reference, and advanced usage patterns.
Features #
Polly Dart provides the following resilience strategies:
Reactive Strategies #
- Retry: Automatically retry failed operations with configurable backoff strategies
- Circuit Breaker: Prevent cascading failures by temporarily blocking calls to failing services
- Fallback: Provide alternative responses when operations fail
- Hedging: Execute multiple parallel attempts and use the fastest successful response
Proactive Strategies #
- Timeout: Cancel operations that take too long
- Rate Limiter: Control the rate of operations to prevent overload
Quick Start #
Add the dependency to your pubspec.yaml
:
dependencies:
polly_dart: ^0.0.6
Basic Usage #
import 'package:polly_dart/polly_dart.dart';
// Create a resilience pipeline
final pipeline = ResiliencePipelineBuilder()
.addRetry(RetryStrategyOptions(maxRetryAttempts: 3))
.addTimeout(Duration(seconds: 10))
.build();
// Execute with resilience
final result = await pipeline.execute((context) async {
// Your code here
return await someAsyncOperation();
});
Examples #
Check out the example directory for comprehensive usage examples.
Real-World Usage Scenarios #
Flutter Frontend Applications #
Flutter apps often need resilience when calling APIs, loading images, or handling user interactions. Here are practical examples:
1. API Client with Resilience
import 'package:polly_dart/polly_dart.dart';
import 'package:http/http.dart' as http;
class ApiClient {
static final _pipeline = ResiliencePipelineBuilder()
// Retry transient HTTP errors
.addRetry(RetryStrategyOptions<http.Response>(
maxRetryAttempts: 3,
delay: Duration(milliseconds: 500),
backoffType: DelayBackoffType.exponential,
shouldHandle: PredicateBuilder<http.Response>()
.handleResultIf((response) => response.statusCode >= 500)
.handleException<http.ClientException>()
.build(),
onRetry: (args) async {
print('Retrying API call, attempt ${args.attemptNumber + 1}');
},
))
// Timeout for slow requests
.addTimeout(Duration(seconds: 30))
// Fallback to cached data
.addFallback(FallbackStrategyOptions<http.Response>(
fallbackAction: (args) async {
// Return cached response or error response
final cachedData = await getCachedResponse();
return Outcome.fromResult(cachedData);
},
onFallback: (args) async {
print('Using cached data due to API failure');
},
))
.build();
static Future<Map<String, dynamic>> fetchUserData(String userId) async {
final response = await _pipeline.execute((context) async {
return await http.get(
Uri.parse('https://api.example.com/users/$userId'),
headers: {'Authorization': 'Bearer $token'},
);
});
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to load user data');
}
}
}
2. Image Loading with Circuit Breaker
class ImageService {
static final _circuitBreaker = ResiliencePipelineBuilder()
.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: 0.6,
minimumThroughput: 5,
samplingDuration: Duration(minutes: 1),
breakDuration: Duration(seconds: 30),
onOpened: (args) async {
print('Image service circuit breaker opened - using placeholders');
},
onClosed: (args) async {
print('Image service circuit breaker closed - service recovered');
},
))
.addTimeout(Duration(seconds: 10))
.build();
static Future<Widget> loadImage(String imageUrl) async {
try {
await _circuitBreaker.execute((context) async {
// Simulate image loading
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode != 200) {
throw Exception('Failed to load image');
}
return response.bodyBytes;
});
return Image.network(imageUrl);
} catch (e) {
// Return placeholder image on failure
return Icon(Icons.image_not_supported, size: 100);
}
}
}
3. User Action Rate Limiting
class LikeButton extends StatefulWidget {
final String postId;
const LikeButton({Key? key, required this.postId}) : super(key: key);
@override
State<LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton> {
static final _rateLimiter = ResiliencePipelineBuilder()
.addRateLimiter(RateLimiterStrategyOptions(
limiterType: RateLimiterType.slidingWindow,
permitLimit: 5, // Max 5 likes per minute
window: Duration(minutes: 1),
onRejected: (args) async {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Please wait before liking again')),
);
},
))
.build();
Future<void> _handleLike() async {
try {
await _rateLimiter.execute((context) async {
await ApiClient.likePost(widget.postId);
setState(() {
// Update UI
});
});
} catch (e) {
// Rate limited - feedback already shown via onRejected
}
}
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: _handleLike,
icon: Icon(Icons.favorite),
);
}
}
Backend Applications #
Server applications need resilience for database calls, external service integrations, and handling high load scenarios:
1. Database Operations with Retry
import 'package:postgres/postgres.dart';
import 'package:polly_dart/polly_dart.dart';
class DatabaseService {
static final _dbPipeline = ResiliencePipelineBuilder()
.addRetry(RetryStrategyOptions(
maxRetryAttempts: 3,
delay: Duration(milliseconds: 100),
backoffType: DelayBackoffType.exponential,
shouldHandle: PredicateBuilder()
.handleException<PostgreSQLException>()
.handleException<SocketException>()
.build(),
onRetry: (args) async {
print('Database operation failed, retrying... (${args.attemptNumber + 1}/3)');
},
))
.addTimeout(Duration(seconds: 30))
.build();
static final Connection _connection = // ... initialize connection
static Future<List<Map<String, dynamic>>> getUsers() async {
return await _dbPipeline.execute((context) async {
final result = await _connection.execute('SELECT * FROM users');
return result.map((row) => row.toColumnMap()).toList();
});
}
static Future<void> createUser(Map<String, dynamic> userData) async {
await _dbPipeline.execute((context) async {
await _connection.execute(
'INSERT INTO users (name, email) VALUES (@name, @email)',
parameters: userData,
);
});
}
}
2. External Service Integration with Circuit Breaker
class PaymentService {
static final _paymentPipeline = ResiliencePipelineBuilder()
.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: 0.5,
minimumThroughput: 10,
samplingDuration: Duration(minutes: 5),
breakDuration: Duration(minutes: 2),
onOpened: (args) async {
// Notify monitoring system
await NotificationService.alertOps('Payment service circuit breaker opened');
},
))
.addRetry(RetryStrategyOptions(
maxRetryAttempts: 2,
delay: Duration(seconds: 1),
))
.addTimeout(Duration(seconds: 45))
.addFallback(FallbackStrategyOptions(
fallbackAction: (args) async {
// Queue payment for later processing
await PaymentQueue.enqueue(args.context.properties['payment']);
return Outcome.fromResult({'status': 'queued', 'id': 'pending'});
},
))
.build();
static Future<Map<String, dynamic>> processPayment(PaymentRequest request) async {
return await _paymentPipeline.execute((context) async {
// Store payment in context for fallback
context.properties['payment'] = request;
final response = await http.post(
Uri.parse('https://payment-gateway.com/charge'),
headers: {'Content-Type': 'application/json'},
body: json.encode(request.toJson()),
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw PaymentException('Payment failed: ${response.statusCode}');
}
});
}
}
3. API Rate Limiting and Load Management
class ApiController {
// Global rate limiter for API endpoints
static final _globalRateLimiter = ResiliencePipelineBuilder()
.addRateLimiter(RateLimiterStrategyOptions(
limiterType: RateLimiterType.concurrency,
permitLimit: 100, // Max 100 concurrent requests
onRejected: (args) async {
print('Request rejected due to high load');
},
))
.build();
// User-specific rate limiter
static final _userRateLimiter = ResiliencePipelineBuilder()
.addRateLimiter(RateLimiterStrategyOptions(
limiterType: RateLimiterType.tokenBucket,
permitLimit: 1000, // 1000 requests
replenishmentPeriod: Duration(hours: 1), // per hour
tokensPerPeriod: 1000,
))
.build();
static Future<Response> handleApiRequest(Request request) async {
final userId = extractUserId(request);
try {
// Apply global rate limiting
await _globalRateLimiter.execute((context) async {
// Apply user-specific rate limiting
await _userRateLimiter.execute((userContext) async {
return await processRequest(request);
});
});
return Response.ok('Success');
} on RateLimitExceededException {
return Response(429, body: 'Rate limit exceeded');
} catch (e) {
return Response.internalServerError(body: 'Internal error');
}
}
}
4. Microservice Communication with Hedging
class OrderService {
static final _hedgingPipeline = ResiliencePipelineBuilder()
.addHedging(HedgingStrategyOptions(
maxHedgedAttempts: 2,
delay: Duration(milliseconds: 500),
onHedging: (args) async {
print('Starting hedged attempt ${args.attemptNumber + 1} for order processing');
},
))
.addTimeout(Duration(seconds: 10))
.build();
static Future<OrderResult> processOrder(Order order) async {
return await _hedgingPipeline.execute((context) async {
// This will try multiple inventory services in parallel
final inventoryResult = await InventoryService.reserveItems(order.items);
final paymentResult = await PaymentService.processPayment(order.payment);
return OrderResult(
orderId: order.id,
inventoryReservation: inventoryResult,
paymentConfirmation: paymentResult,
);
});
}
}
These examples show how Polly Dart can be integrated into real applications to handle common resilience scenarios in both frontend and backend contexts.
Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
License #
This project is licensed under the BSD 3-Clause License - see the LICENSE file for details.