Skip to main content

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
});