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.
example/polly_dart_example.dart
import 'dart:async';
import 'dart:math';
import 'package:polly_dart/polly_dart.dart';
/// Example demonstrating various Polly resilience strategies.
void main() async {
print('Polly Dart Examples');
print('===================\n');
await retryExample();
await timeoutExample();
await circuitBreakerExample();
await fallbackExample();
await hedgingExample();
await rateLimiterExample();
await combinedStrategiesExample();
await basicCachingExample();
await customKeyGenerationExample();
await cacheWithCallbacksExample();
await cacheWithOtherStrategiesExample();
await advancedCacheConfigurationExample();
await cacheMetricsExample();
}
/// Demonstrates retry strategy with exponential backoff.
Future<void> retryExample() async {
print('1. Retry Strategy Example');
print('-------------------------');
final pipeline = ResiliencePipelineBuilder()
.addRetry(RetryStrategyOptions(
maxRetryAttempts: 3,
delay: Duration(milliseconds: 500),
backoffType: DelayBackoffType.exponential,
useJitter: true,
onRetry: (args) async {
print(
' Retry attempt ${args.attemptNumber + 1} after ${args.delay.inMilliseconds}ms delay');
},
))
.build();
try {
final result = await pipeline.execute((context) async {
print(' Executing potentially failing operation...');
// Simulate a flaky operation (70% failure rate)
if (Random().nextDouble() < 0.7) {
throw Exception('Simulated transient failure');
}
return 'Success!';
});
print(' Result: $result');
} catch (e) {
print(' Final failure: $e');
}
print('');
}
/// Demonstrates timeout strategy.
Future<void> timeoutExample() async {
print('2. Timeout Strategy Example');
print('---------------------------');
final pipeline =
ResiliencePipelineBuilder().addTimeout(Duration(seconds: 2)).build();
try {
final result = await pipeline.execute((context) async {
print(' Starting slow operation...');
// Simulate a slow operation
await Future.delayed(Duration(seconds: 3));
return 'This should timeout';
});
print(' Result: $result');
} catch (e) {
print(' Timeout occurred: ${e.runtimeType}');
}
print('');
}
/// Demonstrates circuit breaker strategy.
Future<void> circuitBreakerExample() async {
print('3. Circuit Breaker Strategy Example');
print('-----------------------------------');
final stateProvider = CircuitBreakerStateProvider();
final pipeline = ResiliencePipelineBuilder()
.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: 0.5,
minimumThroughput: 3,
samplingDuration: Duration(seconds: 10),
breakDuration: Duration(seconds: 2),
stateProvider: stateProvider,
onOpened: (args) async {
print(' Circuit breaker opened!');
},
onClosed: (args) async {
print(' Circuit breaker closed.');
},
))
.build();
print(' Circuit state: ${stateProvider.circuitState}');
// Execute several failing operations to trigger circuit breaker
for (int i = 0; i < 5; i++) {
try {
await pipeline.execute((context) async {
print(' Attempt ${i + 1}...');
throw Exception('Simulated failure');
});
} catch (e) {
print(' Failed: ${e.runtimeType}');
}
print(' Circuit state: ${stateProvider.circuitState}');
await Future.delayed(Duration(milliseconds: 100));
}
print('');
}
/// Demonstrates fallback strategy.
Future<void> fallbackExample() async {
print('4. Fallback Strategy Example');
print('----------------------------');
final pipeline = TypedResiliencePipelineBuilder<String>()
.addFallback(FallbackStrategyOptions<String>(
fallbackAction: (args) async {
print(' Executing fallback action...');
return Outcome.fromResult('Fallback result');
},
onFallback: (args) async {
print(' Primary operation failed, using fallback');
},
))
.build();
try {
final result = await pipeline.execute((context) async {
print(' Executing primary operation...');
throw Exception('Primary operation failed');
});
print(' Result: $result');
} catch (e) {
print(' Unexpected error: $e');
}
print('');
}
/// Demonstrates hedging strategy.
Future<void> hedgingExample() async {
print('5. Hedging Strategy Example');
print('---------------------------');
final pipeline = TypedResiliencePipelineBuilder<String>()
.addHedging(HedgingStrategyOptions<String>(
maxHedgedAttempts: 2,
delay: Duration(milliseconds: 500),
onHedging: (args) async {
print(' Starting hedged attempt ${args.attemptNumber + 1}...');
},
))
.build();
final stopwatch = Stopwatch()..start();
try {
final result = await pipeline.execute((context) async {
final delay = Duration(milliseconds: 800 + Random().nextInt(400));
print(
' Attempt ${context.attemptNumber + 1} will take ${delay.inMilliseconds}ms');
await Future.delayed(delay);
return 'Result from attempt ${context.attemptNumber + 1}';
});
stopwatch.stop();
print(' Result: $result');
print(' Total time: ${stopwatch.elapsedMilliseconds}ms');
} catch (e) {
print(' Error: $e');
}
print('');
}
/// Demonstrates rate limiter strategy.
Future<void> rateLimiterExample() async {
print('6. Rate Limiter Strategy Example');
print('--------------------------------');
final pipeline = ResiliencePipelineBuilder()
.addRateLimiter(RateLimiterStrategyOptions.slidingWindow(
permitLimit: 3,
window: Duration(seconds: 2),
onRejected: (args) async {
print(' Request rejected: ${args.reason}');
},
))
.build();
// Try to execute 5 requests rapidly
final futures = <Future>[];
for (int i = 0; i < 5; i++) {
futures.add(pipeline.execute((context) async {
print(' Request ${i + 1} executing...');
await Future.delayed(Duration(milliseconds: 100));
return 'Response ${i + 1}';
}).catchError((e) {
return 'Rejected';
}));
}
final results = await Future.wait(futures);
for (int i = 0; i < results.length; i++) {
print(' Request ${i + 1} result: ${results[i]}');
}
print('');
}
/// Demonstrates combining multiple strategies.
Future<void> combinedStrategiesExample() async {
print('7. Combined Strategies Example');
print('------------------------------');
final pipeline = ResiliencePipelineBuilder()
.addRetry(RetryStrategyOptions(
maxRetryAttempts: 2,
delay: Duration(milliseconds: 200),
))
.addTimeout(Duration(seconds: 5))
.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: 0.8,
minimumThroughput: 2,
))
.build();
for (int i = 0; i < 3; i++) {
try {
final result = await pipeline.execute((context) async {
print(' Execution ${i + 1}, attempt ${context.attemptNumber + 1}');
// Simulate occasional failures
if (Random().nextDouble() < 0.6) {
throw Exception('Simulated failure');
}
return 'Success on execution ${i + 1}';
});
print(' Result: $result');
} catch (e) {
print(' Failed: ${e.runtimeType}');
}
await Future.delayed(Duration(milliseconds: 100));
}
print('');
print('Examples completed!');
}
/// Example 1: Basic Memory Caching
///
/// Demonstrates the simplest cache setup with default configuration.
Future<void> basicCachingExample() async {
print('\n=== Basic Caching Example ===');
// Create a pipeline with basic memory caching
final cacheProvider = MemoryCacheProvider(
defaultTtl: Duration(minutes: 5),
maxSize: 1000,
);
final pipeline = ResiliencePipelineBuilder()
.addCache(CacheStrategyOptions<String>(
cache: cacheProvider,
keyGenerator: (context) =>
'expensive-operation', // Fixed key for this example
))
.build();
// Simulate an expensive operation
var callCount = 0;
Future<String> expensiveOperation() async {
callCount++;
print('Executing expensive operation (call #$callCount)');
await Future.delayed(Duration(milliseconds: 100)); // Simulate work
return 'Expensive result $callCount';
}
// First call - will execute the operation and cache the result
print('First call:');
final result1 =
await pipeline.execute<String>((context) => expensiveOperation());
print('Result: $result1');
print('Call count: $callCount\n');
// Second call - will return cached result without executing operation
print('Second call (should use cache):');
final result2 =
await pipeline.execute<String>((context) => expensiveOperation());
print('Result: $result2');
print('Call count: $callCount (should still be 1)');
}
/// Example 2: Custom Key Generation
///
/// Shows how to use custom cache keys for different contexts.
Future<void> customKeyGenerationExample() async {
print('\n=== Custom Key Generation Example ===');
// Create cache provider
final cacheProvider = MemoryCacheProvider(
maxSize: 100,
defaultTtl: Duration(minutes: 10),
);
// Pipeline with custom key generation based on user context
final pipeline = ResiliencePipelineBuilder()
.addCache(CacheStrategyOptions<String>(
cache: cacheProvider,
keyGenerator: (context) {
final userId = context.getProperty<String>('userId');
final operation = context.getProperty<String>('operation');
return 'user:$userId:$operation';
},
ttl: Duration(seconds: 30),
))
.build();
// Helper to create user-specific contexts
ResilienceContext createUserContext(String userId, String operation) {
final context = ResilienceContext();
context.setProperty('userId', userId);
context.setProperty('operation', operation);
return context;
}
var callCount = 0;
Future<String> getUserData(String userId) async {
callCount++;
print('Fetching data for user $userId (call #$callCount)');
return 'Data for user $userId';
}
// Call for user1 - will cache with key "user:user1:getData"
print('Getting data for user1 (first time):');
final context1 = createUserContext('user1', 'getData');
final result1 = await pipeline.execute(
(ctx) => getUserData('user1'),
context: context1,
);
print('Result: $result1');
print('Call count: $callCount\n');
// Call for user2 - different key, will execute
print('Getting data for user2 (different user):');
final context2 = createUserContext('user2', 'getData');
final result2 = await pipeline.execute(
(ctx) => getUserData('user2'),
context: context2,
);
print('Result: $result2');
print('Call count: $callCount\n');
// Call for user1 again - should use cache
print('Getting data for user1 again (should use cache):');
final context3 = createUserContext('user1', 'getData');
final result3 = await pipeline.execute(
(ctx) => getUserData('user1'),
context: context3,
);
print('Result: $result3');
print('Call count: $callCount (should still be 2)');
}
/// Example 3: Cache with Callbacks
///
/// Demonstrates monitoring cache behavior with callbacks.
Future<void> cacheWithCallbacksExample() async {
print('\n=== Cache with Callbacks Example ===');
var hitCount = 0;
var missCount = 0;
var setCount = 0;
final pipeline = ResiliencePipelineBuilder()
.addCache(CacheStrategyOptions<String>(
cache: MemoryCacheProvider(defaultTtl: Duration(seconds: 5)),
keyGenerator: (context) => 'api-call',
onHit: (args) async {
hitCount++;
print('ðŊ Cache HIT for key: ${args.key} -> ${args.value}');
},
onMiss: (args) async {
missCount++;
print('â Cache MISS for key: ${args.key}');
},
onSet: (args) async {
setCount++;
print('ðū Cache SET for key: ${args.key} -> ${args.value}');
},
))
.build();
Future<String> apiCall() async {
print('Making API call...');
await Future.delayed(Duration(milliseconds: 50));
return 'API Response ${DateTime.now().millisecondsSinceEpoch}';
}
// First call - cache miss
print('Call 1 (cache miss expected):');
await pipeline.execute((ctx) => apiCall());
// Second call - cache hit
print('\nCall 2 (cache hit expected):');
await pipeline.execute((ctx) => apiCall());
// Wait for TTL to expire
print('\nWaiting for cache to expire...');
await Future.delayed(Duration(seconds: 6));
// Third call - cache miss due to expiration
print('\nCall 3 (cache miss due to expiration):');
await pipeline.execute((ctx) => apiCall());
print('\nð Final stats:');
print('Cache hits: $hitCount');
print('Cache misses: $missCount');
print('Cache sets: $setCount');
}
/// Example 4: Cache with Other Strategies
///
/// Shows how caching integrates with retry, timeout, and circuit breaker.
Future<void> cacheWithOtherStrategiesExample() async {
print('\n=== Cache with Other Strategies Example ===');
// Pipeline combining multiple strategies
final pipeline = ResiliencePipelineBuilder()
.addRetry(RetryStrategyOptions(
maxRetryAttempts: 3,
delay: Duration(milliseconds: 100),
))
.addTimeout(Duration(seconds: 2))
.addCache(CacheStrategyOptions<String>(
cache: MemoryCacheProvider(defaultTtl: Duration(minutes: 1)),
keyGenerator: (context) => 'resilient-api-call',
onHit: (args) async => print('â
Using cached result: ${args.value}'),
onMiss: (args) async =>
print('ð Cache miss, executing with resilience...'),
))
.build();
var attemptCount = 0;
Future<String> unreliableApiCall() async {
attemptCount++;
print('API attempt #$attemptCount');
// Fail first 2 attempts, succeed on 3rd
if (attemptCount < 3) {
throw Exception('API temporarily unavailable');
}
return 'Success after retries!';
}
// First call - will retry and eventually succeed, then cache
print('First call (will retry and cache result):');
try {
final result1 = await pipeline.execute((ctx) => unreliableApiCall());
print('Result: $result1');
} catch (e) {
print('Failed: $e');
}
// Reset attempt counter for second call
attemptCount = 0;
// Second call - should use cache, no retries needed
print('\nSecond call (should use cache, no retries):');
try {
final result2 = await pipeline.execute((ctx) => unreliableApiCall());
print('Result: $result2');
print('Attempt count: $attemptCount (should be 0 due to cache hit)');
} catch (e) {
print('Failed: $e');
}
}
/// Example 5: Advanced Cache Configuration
///
/// Demonstrates advanced caching features and edge cases.
Future<void> advancedCacheConfigurationExample() async {
print('\n=== Advanced Cache Configuration Example ===');
// Cache with custom shouldCache predicate
final pipeline = ResiliencePipelineBuilder()
.addCache(CacheStrategyOptions<Map<String, dynamic>>(
cache: MemoryCacheProvider(
maxSize: 50,
defaultTtl: Duration(seconds: 10),
),
keyGenerator: (context) {
final endpoint = context.getProperty<String>('endpoint');
final params = context.getProperty<Map<String, String>>('params');
final paramString =
params?.entries.map((e) => '${e.key}=${e.value}').join('&') ?? '';
return '$endpoint?$paramString';
},
shouldCache: (outcome) {
// Only cache successful responses with data
if (!outcome.hasResult) return false;
final data = outcome.result;
return data.isNotEmpty && data['status'] == 'success';
},
onHit: (args) async => print('ð Cache hit for: ${args.key}'),
onMiss: (args) async => print('ð Cache miss for: ${args.key}'),
ttl: Duration(seconds: 15),
))
.build();
Future<Map<String, dynamic>> apiCall(
String endpoint, Map<String, String> params, bool shouldSucceed) async {
print('Calling $endpoint with params: $params');
await Future.delayed(Duration(milliseconds: 100));
if (shouldSucceed) {
return {
'status': 'success',
'data': {
'endpoint': endpoint,
'timestamp': DateTime.now().toIso8601String()
},
'params': params,
};
} else {
return {
'status': 'error',
'message': 'Something went wrong',
};
}
}
// Helper to create context with endpoint and params
ResilienceContext createApiContext(
String endpoint, Map<String, String> params) {
final context = ResilienceContext();
context.setProperty('endpoint', endpoint);
context.setProperty('params', params);
return context;
}
// Call 1: Successful call - will be cached
print('Call 1 (successful, will be cached):');
final context1 = createApiContext('/users', {'page': '1', 'limit': '10'});
final result1 = await pipeline.execute(
(ctx) => apiCall('/users', {'page': '1', 'limit': '10'}, true),
context: context1,
);
print('Result: ${result1['status']}\n');
// Call 2: Same endpoint and params - should use cache
print('Call 2 (same params, should use cache):');
final context2 = createApiContext('/users', {'page': '1', 'limit': '10'});
final result2 = await pipeline.execute(
(ctx) => apiCall('/users', {'page': '1', 'limit': '10'}, true),
context: context2,
);
print('Result: ${result2['status']}\n');
// Call 3: Error response - should NOT be cached
print('Call 3 (error response, should not be cached):');
final context3 = createApiContext('/users', {'page': '2', 'limit': '10'});
final result3 = await pipeline.execute(
(ctx) => apiCall('/users', {'page': '2', 'limit': '10'}, false),
context: context3,
);
print('Result: ${result3['status']}\n');
// Call 4: Same as call 3 - should execute again (not cached due to error)
print('Call 4 (same as error call, should execute again):');
final context4 = createApiContext('/users', {'page': '2', 'limit': '10'});
final result4 = await pipeline.execute(
(ctx) => apiCall('/users', {'page': '2', 'limit': '10'}, false),
context: context4,
);
print('Result: ${result4['status']}');
}
/// Example 6: Cache Metrics (Basic Implementation)
///
/// Demonstrates basic cache metrics collection.
Future<void> cacheMetricsExample() async {
print('\n=== Cache Metrics Example ===');
// Create cache provider with metrics collection
final baseCacheProvider =
MemoryCacheProvider(defaultTtl: Duration(seconds: 5));
final metricsCache = MetricsCollectingCacheProvider(baseCacheProvider);
final pipeline = ResiliencePipelineBuilder()
.addCache(CacheStrategyOptions<String>(
cache: metricsCache,
keyGenerator: (context) => 'metrics-test',
onHit: (args) async {
print('â
Cache hit: ${args.value}');
},
onMiss: (args) async {
print('â Cache miss for key: ${args.key}');
},
))
.build();
Future<String> operation() async {
await Future.delayed(Duration(milliseconds: 50));
return 'Operation result';
}
// Execute multiple operations
for (int i = 1; i <= 5; i++) {
print('\nOperation $i:');
await pipeline.execute<String>((ctx) => operation());
if (i == 2) {
// Clear cache after second operation to demonstrate miss behavior
print('Clearing cache...');
await metricsCache.clear();
}
}
// Print final metrics
print('\nð Final Cache Metrics:');
final metrics = metricsCache.metrics;
print('Total operations: ${metrics.totalOperations}');
print('Cache hits: ${metrics.hits}');
print('Cache misses: ${metrics.misses}');
print('Cache sets: ${metrics.sets}');
print('Hit ratio: ${(metrics.hitRatio * 100).toStringAsFixed(1)}%');
print('Average hit time: ${metrics.averageHitTime.inMicroseconds}Ξs');
print('Average miss time: ${metrics.averageMissTime.inMicroseconds}Ξs');
}
/// Simple cache metrics collector (kept for backwards compatibility)
class CacheMetrics {
int hits = 0;
int misses = 0;
void recordHit() => hits++;
void recordMiss() => misses++;
int get totalOperations => hits + misses;
double get hitRatio => totalOperations > 0 ? hits / totalOperations : 0.0;
}