Skip to content
Open
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,41 @@ fmt.Println("This is a hystrix configured circuit", c.Name())
// Output: This is a hystrix configured circuit hystrix-circuit
```

## Adaptive Hystrix (`closers/hystrix-adaptive`)

The [hystrix](https://pkg.go.dev/github.com/cep21/circuit/v4/closers/hystrix) opener trips the breaker when enough requests fail. That is what you want when the dependency is actually unhealthy—but when *everything* is simply a bit slow, many calls may time out and look like total failure even though nothing is uniquely broken. Package **`hystrixadaptive`** (`import "github.com/cep21/circuit/v4/closers/hystrix-adaptive"`) layers on top of the usual Hystrix opener: it can be more patient in that situation (mostly timeouts, not hard errors), give a little slack while the outage pattern looks like blanket slowness, and tighten again when fast successes return. You tune how much slack is allowed and how quickly it grows or shrinks. It only affects **when** the breaker opens; **per-call** deadlines are still whatever you set on **`Execution.Timeout`**.

```go
configuration := hystrixadaptive.Factory{
Factory: hystrix.Factory{
ConfigureCloser: hystrix.ConfigureCloser{
// Same half-open / sleep window behavior as plain Hystrix
},
},
ConfigureAdaptive: hystrixadaptive.ConfigureAdaptive{
ConfigureOpener: hystrix.ConfigureOpener{
RequestVolumeThreshold: 10,
},
// Expected healthy latency; "fast" successes decay headroom below this.
BaselineLatency: 100 * time.Millisecond,
// Cap on accumulated headroom (e.g. allow up to baseline + 200ms conceptually).
MaxExtraLatency: 200 * time.Millisecond,
// Added on each timeout and on successes slower than baseline+headroom.
IncreaseExtra: 10 * time.Millisecond,
// Subtracted when a success finishes faster than BaselineLatency.
DecreaseExtra: 10 * time.Millisecond,
// While headroom > 0, defer opening if timeouts/(timeouts+failures) is at least this ratio.
MinTimeoutRatioToDefer: 0.85,
},
}
h := circuit.Manager{
DefaultCircuitProperties: []circuit.CommandPropertiesConstructor{configuration.Configure},
}
c := h.MustCreateCircuit("adaptive-hystrix")
fmt.Println("This circuit uses the adaptive Hystrix opener", c.Name())
// Output: This circuit uses the adaptive Hystrix opener adaptive-hystrix
```

## [Enable dashboard metrics](https://godoc.org/github.com/cep21/circuit/metriceventstream#example-MetricEventStream)

Dashboard metrics can be enabled with the MetricEventStream object. This example creates an event stream handler,
Expand Down
80 changes: 80 additions & 0 deletions closers/hystrix-adaptive/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package hystrixadaptive

import (
"time"

"github.com/cep21/circuit/v4"
"github.com/cep21/circuit/v4/closers/hystrix"
)

// ConfigureAdaptive holds adaptive policy and embeds hystrix.ConfigureOpener; it does not set circuit execution timeouts
type ConfigureAdaptive struct {
hystrix.ConfigureOpener

// BaselineLatency is the nominal fast path; successes below it decrease extra
BaselineLatency time.Duration
// MaxExtraLatency caps extra; slow-success threshold is roughly baseline+extra; at the cap, timeout deferral ends if inner ShouldOpen
MaxExtraLatency time.Duration
// IncreaseExtra added to extra on each timeout and on each success slower than baseline+current extra
IncreaseExtra time.Duration
// DecreaseExtra subtracted from extra when a success finishes faster than BaselineLatency
DecreaseExtra time.Duration
// MinTimeoutRatioToDefer is the rolling timeouts/(timeouts+failures) above which ShouldOpen may defer while 0 < extra < MaxExtraLatency
MinTimeoutRatioToDefer float64
}

// Merge copies missing fields from other
func (c *ConfigureAdaptive) Merge(other ConfigureAdaptive) {
c.ConfigureOpener.Merge(other.ConfigureOpener)
if c.BaselineLatency == 0 {
c.BaselineLatency = other.BaselineLatency
}
if c.MaxExtraLatency == 0 {
c.MaxExtraLatency = other.MaxExtraLatency
}
if c.IncreaseExtra == 0 {
c.IncreaseExtra = other.IncreaseExtra
}
if c.DecreaseExtra == 0 {
c.DecreaseExtra = other.DecreaseExtra
}
if c.MinTimeoutRatioToDefer == 0 {
c.MinTimeoutRatioToDefer = other.MinTimeoutRatioToDefer
}
}

// defaultConfigureAdaptive is the default adaptive configuration
var defaultConfigureAdaptive = ConfigureAdaptive{
ConfigureOpener: hystrix.ConfigureOpener{
RequestVolumeThreshold: 20,
ErrorThresholdPercentage: 50,
Now: time.Now,
NumBuckets: 10,
RollingDuration: 10 * time.Second,
},
BaselineLatency: 100 * time.Millisecond,
MaxExtraLatency: 200 * time.Millisecond,
IncreaseExtra: 10 * time.Millisecond,
DecreaseExtra: 10 * time.Millisecond,
MinTimeoutRatioToDefer: 0.85,
}

// Factory merges hystrix.Factory with adaptive configuration
type Factory struct {
hystrix.Factory

ConfigureAdaptive ConfigureAdaptive
CreateConfigureAdaptive []func(circuitName string) ConfigureAdaptive
}

// Configure returns circuit.Config with adaptive ClosedToOpen from this factory
func (f *Factory) Configure(circuitName string) circuit.Config {
cfg := f.Factory.Configure(circuitName)
adaptiveCfg := ConfigureAdaptive{}
for i := len(f.CreateConfigureAdaptive) - 1; i >= 0; i-- {
adaptiveCfg.Merge(f.CreateConfigureAdaptive[i](circuitName))
}
adaptiveCfg.Merge(f.ConfigureAdaptive)
cfg.General.ClosedToOpenFactory = OpenerFactory(adaptiveCfg)
return cfg
}
9 changes: 9 additions & 0 deletions closers/hystrix-adaptive/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Package hystrixadaptive implements ClosedToOpen by wrapping the Hystrix opener and
deferring trips when failures are mostly timeouts while headroom is below its cap

Additive headroom "extra" sits on top of BaselineLatency; timeouts and slow successes
raise it (capped by MaxExtraLatency); successes faster than BaselineLatency lower it;
set per-request deadlines on circuit.Execution, not in this package
*/
package hystrixadaptive
67 changes: 67 additions & 0 deletions closers/hystrix-adaptive/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package hystrixadaptive_test

import (
"fmt"
"time"

"github.com/cep21/circuit/v4"
"github.com/cep21/circuit/v4/closers/hystrix"
hystrixadaptive "github.com/cep21/circuit/v4/closers/hystrix-adaptive"
)

// ExampleFactory wires circuit.Manager with adaptive ClosedToOpen on top of hystrix.Factory
func ExampleFactory() {
configuration := hystrixadaptive.Factory{
Factory: hystrix.Factory{
ConfigureOpener: hystrix.ConfigureOpener{
RequestVolumeThreshold: 10,
},
ConfigureCloser: hystrix.ConfigureCloser{},
},
ConfigureAdaptive: hystrixadaptive.ConfigureAdaptive{
BaselineLatency: 100 * time.Millisecond,
MaxExtraLatency: 200 * time.Millisecond,
IncreaseExtra: 10 * time.Millisecond,
DecreaseExtra: 10 * time.Millisecond,
MinTimeoutRatioToDefer: 0.85,
},
}
h := circuit.Manager{
DefaultCircuitProperties: []circuit.CommandPropertiesConstructor{configuration.Configure},
}
c := h.MustCreateCircuit("adaptive-hystrix")
fmt.Println("circuit:", c.Name())
// Output:
// circuit: adaptive-hystrix
}

// ExampleOpenerFactory builds a [circuit.Config] with OpenerFactory and a Hystrix closer
func ExampleOpenerFactory() {
cfg := circuit.Config{
General: circuit.GeneralConfig{
ClosedToOpenFactory: hystrixadaptive.OpenerFactory(hystrixadaptive.ConfigureAdaptive{
ConfigureOpener: hystrix.ConfigureOpener{
RequestVolumeThreshold: 10,
},
}),
OpenToClosedFactory: hystrix.CloserFactory(hystrix.ConfigureCloser{}),
},
}
c := circuit.NewCircuitFromConfig("custom-opener", cfg)
fmt.Println("circuit:", c.Name())
// Output:
// circuit: custom-opener
}

// ExampleOpener_SetConfigThreadSafe updates adaptive fields via SetConfigThreadSafe; use NewOpener for a concrete *Opener
func ExampleOpener_SetConfigThreadSafe() {
ao := hystrixadaptive.NewOpener(hystrixadaptive.ConfigureAdaptive{})
fmt.Println("default baseline:", ao.Config().BaselineLatency)
ao.SetConfigThreadSafe(hystrixadaptive.ConfigureAdaptive{
BaselineLatency: 50 * time.Millisecond,
})
fmt.Println("new baseline:", ao.Config().BaselineLatency)
// Output:
// default baseline: 100ms
// new baseline: 50ms
}
Loading