HedgingStrategy
The HedgingStrategy
improves performance by executing multiple parallel attempts and returning the first successful result, reducing latency caused by slow responses.
Overview
The hedging strategy executes the primary operation and, after a delay, starts additional parallel attempts. The first successful result is returned, while other attempts are cancelled. This is particularly useful for reducing tail latency in distributed systems.
class HedgingStrategy<T> extends ResilienceStrategy<T> {
HedgingStrategy(HedgingStrategyOptions<T> options);
Future<Outcome<T>> executeCore<T>(
ResilienceCallback<T> callback,
ResilienceContext context,
ResilienceCallback<T> next,
);
}
class HedgingStrategyOptions<T> {
const HedgingStrategyOptions({
this.maxHedgedAttempts = 1,
this.delay = const Duration(milliseconds: 100),
this.shouldHandle,
this.onHedging,
});
}
HedgingStrategyOptions Properties
maxHedgedAttempts
Maximum number of additional hedged attempts to execute.
Type: int
Default: 1
HedgingStrategyOptions(
maxHedgedAttempts: 2, // Total of 3 attempts (1 primary + 2 hedged)
)
delay
Delay before starting each hedged attempt.
Type: Duration
Default: Duration(milliseconds: 100)
HedgingStrategyOptions(
delay: Duration(milliseconds: 50),
)
shouldHandle
Predicate to determine which outcomes should trigger hedging.
Type: ShouldHandlePredicate<T>?
Default: null
(hedges for all outcomes)
HedgingStrategyOptions(
shouldHandle: (outcome) =>
outcome.hasException ||
isSlowResponse(outcome),
)
onHedging
Callback invoked when a hedged attempt is started.
Type: OnHedgingCallback<T>?
Default: null
HedgingStrategyOptions(
onHedging: (context, args) {
logger.debug('Started hedged attempt ${args.attemptNumber}');
},
)
Callback Types
ShouldHandlePredicate<T>
typedef ShouldHandlePredicate<T> = bool Function(Outcome<T> outcome);
Determines whether hedging should be triggered for a given outcome.
OnHedgingCallback<T>
typedef OnHedgingCallback<T> = void Function(
ResilienceContext context,
OnHedgingArgs args,
);
Called when a hedged attempt is started.
OnHedgingArgs Properties:
attemptNumber
- The number of the hedged attempt (1, 2, 3...)delay
- The delay that was applied before starting this attempt
Usage Examples
Basic Hedging
final hedgingStrategy = HedgingStrategy(HedgingStrategyOptions(
maxHedgedAttempts: 2,
delay: Duration(milliseconds: 100),
));
final pipeline = ResiliencePipelineBuilder()
.addStrategy(hedgingStrategy)
.build();
Aggressive Hedging for Low Latency
final hedgingStrategy = HedgingStrategy(HedgingStrategyOptions(
maxHedgedAttempts: 3,
delay: Duration(milliseconds: 50), // Quick hedging
));
// Use for time-critical operations
final result = await pipeline.execute((context) async {
return await criticalApiCall();
});
Conservative Hedging
final hedgingStrategy = HedgingStrategy(HedgingStrategyOptions(
maxHedgedAttempts: 1,
delay: Duration(milliseconds: 200), // Wait longer before hedging
));
Conditional Hedging
final hedgingStrategy = HedgingStrategy(HedgingStrategyOptions(
maxHedgedAttempts: 2,
delay: Duration(milliseconds: 100),
shouldHandle: (outcome) {
// Only hedge if the primary request is taking too long
// (This would need custom logic to track timing)
return true; // Simplified example
},
));
Hedging with Monitoring
final hedgingStrategy = HedgingStrategy(HedgingStrategyOptions(
maxHedgedAttempts: 2,
delay: Duration(milliseconds: 75),
onHedging: (context, args) {
final operation = context.getProperty<String>('operation') ?? 'unknown';
logger.debug('Hedging attempt ${args.attemptNumber} for $operation');
// Track hedging metrics
metrics.incrementCounter('hedging_attempts', {
'operation': operation,
'attempt': args.attemptNumber.toString(),
});
},
));
Database Query Hedging
final dbHedgingStrategy = HedgingStrategy(HedgingStrategyOptions(
maxHedgedAttempts: 1, // Conservative for databases
delay: Duration(milliseconds: 200), // Allow time for primary query
onHedging: (context, args) {
final query = context.getProperty<String>('query');
logger.warn('Database query is slow, starting hedged attempt: $query');
},
));
final dbPipeline = ResiliencePipelineBuilder()
.addStrategy(dbHedgingStrategy)
.build();
// Usage with context
final context = ResilienceContext();
context.setProperty('query', 'SELECT * FROM users WHERE active = true');
final users = await dbPipeline.execute(
(context) => database.query(query),
context: context,
);
HTTP Request Hedging
final httpHedgingStrategy = HedgingStrategy(HedgingStrategyOptions(
maxHedgedAttempts: 2,
delay: Duration(milliseconds: 100),
onHedging: (context, args) {
final url = context.getProperty<String>('url');
logger.info('HTTP request hedging started for $url');
},
));
final httpPipeline = ResiliencePipelineBuilder()
.addStrategy(httpHedgingStrategy)
.addTimeout(Duration(seconds: 5)) // Per-attempt timeout
.build();
// Usage
final context = ResilienceContext();
context.setProperty('url', 'https://api.example.com/data');
final response = await httpPipeline.execute(
(context) => httpClient.get('https://api.example.com/data'),
context: context,
);
Microservice Call Hedging
final serviceHedgingStrategy = HedgingStrategy(HedgingStrategyOptions(
maxHedgedAttempts: 2,
delay: Duration(milliseconds: 150),
onHedging: (context, args) {
final service = context.getProperty<String>('serviceName');
final method = context.getProperty<String>('method');
logger.info('Hedging $service.$method (attempt ${args.attemptNumber})');
// Track which services need hedging most
serviceMetrics.recordHedging(service, method);
},
));
// Different hedging for different service types
final criticalServicePipeline = ResiliencePipelineBuilder()
.addStrategy(HedgingStrategy(HedgingStrategyOptions(
maxHedgedAttempts: 3, // More aggressive for critical services
delay: Duration(milliseconds: 50),
)))
.build();
final normalServicePipeline = ResiliencePipelineBuilder()
.addStrategy(HedgingStrategy(HedgingStrategyOptions(
maxHedgedAttempts: 1, // Conservative for normal services
delay: Duration(milliseconds: 200),
)))
.build();
File Download Hedging
final downloadHedgingStrategy = HedgingStrategy(HedgingStrategyOptions(
maxHedgedAttempts: 2,
delay: Duration(seconds: 1), // Longer delay for downloads
onHedging: (context, args) {
final fileName = context.getProperty<String>('fileName');
final fileSize = context.getProperty<int>('fileSize');
logger.info('Download hedging: $fileName (${fileSize} bytes)');
},
));
final downloadPipeline = ResiliencePipelineBuilder()
.addStrategy(downloadHedgingStrategy)
.addTimeout(Duration(minutes: 5)) // Long timeout for downloads
.build();
Search Query Hedging
final searchHedgingStrategy = HedgingStrategy(HedgingStrategyOptions(
maxHedgedAttempts: 2,
delay: Duration(milliseconds: 100),
onHedging: (context, args) {
final query = context.getProperty<String>('searchQuery');
logger.debug('Search hedging: "$query" (attempt ${args.attemptNumber})');
},
));
final searchPipeline = ResiliencePipelineBuilder()
.addStrategy(searchHedgingStrategy)
.addTimeout(Duration(seconds: 3))
.build();
// Usage
final results = await searchPipeline.execute((context) async {
context.setProperty('searchQuery', userQuery);
return await searchService.search(userQuery);
});
Hedging with Circuit Breaker
final pipeline = ResiliencePipelineBuilder()
.addHedging(HedgingStrategyOptions(
maxHedgedAttempts: 2,
delay: Duration(milliseconds: 100),
))
.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: 0.5,
minimumThroughput: 10,
))
.build();
Load Balancer Simulation with Hedging
final hedgingStrategy = HedgingStrategy(HedgingStrategyOptions(
maxHedgedAttempts: 2,
delay: Duration(milliseconds: 100),
onHedging: (context, args) {
// Could implement server selection logic here
final servers = context.getProperty<List<String>>('availableServers');
final selectedServer = selectNextServer(servers, args.attemptNumber);
context.setProperty('currentServer', selectedServer);
logger.debug('Hedging to server: $selectedServer');
},
));
Best Practices
Choose Appropriate Delays
// Fast operations - short delay
HedgingStrategyOptions(
delay: Duration(milliseconds: 50),
maxHedgedAttempts: 2,
)
// Network calls - medium delay
HedgingStrategyOptions(
delay: Duration(milliseconds: 100),
maxHedgedAttempts: 2,
)
// Heavy operations - longer delay
HedgingStrategyOptions(
delay: Duration(milliseconds: 500),
maxHedgedAttempts: 1,
)
Limit Hedged Attempts
// Good - reasonable number of attempts
HedgingStrategyOptions(maxHedgedAttempts: 2) // Total: 3 attempts
// Avoid - too many attempts can overwhelm services
HedgingStrategyOptions(maxHedgedAttempts: 10) // Total: 11 attempts
Monitor Hedging Effectiveness
HedgingStrategyOptions(
onHedging: (context, args) {
final startTime = context.getProperty<DateTime>('requestStart');
final hedgingTime = DateTime.now().difference(startTime!);
// Track how long it takes before hedging kicks in
metrics.recordHistogram('hedging_trigger_time', hedgingTime.inMilliseconds);
// Track hedging frequency
metrics.incrementCounter('hedging_triggered');
},
)
Use with Timeouts
// Each attempt should have its own timeout
final pipeline = ResiliencePipelineBuilder()
.addHedging(HedgingStrategyOptions(
maxHedgedAttempts: 2,
delay: Duration(milliseconds: 100),
))
.addTimeout(Duration(seconds: 5)) // Timeout per attempt
.build();
Consider Resource Usage
// For expensive operations, use conservative hedging
HedgingStrategyOptions(
maxHedgedAttempts: 1, // Only one extra attempt
delay: Duration(milliseconds: 500), // Wait longer before hedging
)
// For cheap operations, can be more aggressive
HedgingStrategyOptions(
maxHedgedAttempts: 3, // Multiple extra attempts
delay: Duration(milliseconds: 50), // Hedge quickly
)
Handle Cancellation Properly
// Ensure your operations respect cancellation tokens
Future<String> apiCall(CancellationToken cancellationToken) async {
// Check cancellation before expensive operations
cancellationToken.throwIfCancellationRequested();
final response = await httpClient.get('/api/data');
// Check cancellation before processing
cancellationToken.throwIfCancellationRequested();
return processResponse(response);
}
Test Hedging Scenarios
test('should return first successful hedged result', () async {
// Test that hedging works correctly
when(mockService.call(any))
.thenAnswer((_) async {
await Future.delayed(Duration(seconds: 1)); // Slow primary
return 'primary-result';
});
when(mockService.call(any))
.thenAnswer((_) async => 'hedged-result'); // Fast hedged attempt
final result = await pipeline.execute((context) => mockService.call());
expect(result, equals('hedged-result')); // Should get the faster result
});