├── sample ├── README ├── LICENSE ├── sample_test.go └── sample.go ├── httpexp └── httpexp.go ├── LICENSE ├── executor_bench_test.go ├── statsd ├── LICENSE └── statsd.go ├── readme_test.go ├── circuit_test.go ├── README.md ├── export.go ├── metrics_test.go ├── circuit.go ├── metrics.go ├── executor.go └── executor_test.go /sample/README: -------------------------------------------------------------------------------- 1 | This code is based in https://github.com/rcrowley/go-metrics by Richard Crowley. (Attached the LICENSE) 2 | I didn't use the library itself because I only need a couple of classes (histogram? and sample) -------------------------------------------------------------------------------- /httpexp/httpexp.go: -------------------------------------------------------------------------------- 1 | package httpexp 2 | 3 | import ( 4 | "fmt" 5 | "github.com/dahernan/goHystrix" 6 | "net/http" 7 | ) 8 | 9 | func expvarHandler(w http.ResponseWriter, r *http.Request) { 10 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 11 | fmt.Fprintf(w, goHystrix.Circuits().ToJSON()) 12 | } 13 | 14 | func init() { 15 | http.HandleFunc("/debug/circuits", expvarHandler) 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 David Hernandez 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /executor_bench_test.go: -------------------------------------------------------------------------------- 1 | // Basic benchmark 2 | // go test -test.run="^bench$" -bench=. 3 | // 4 | // BenchmarkRun 50000000 67.1 ns/op 5 | // BenchmarkExecute 500000 7835.0 ns/op 6 | // 7 | package goHystrix 8 | 9 | import ( 10 | "testing" 11 | "time" 12 | ) 13 | 14 | var ( 15 | command = NewCommand("SimpleCommand", "testGroup", &SimpleCommand{}) 16 | ) 17 | 18 | type SimpleCommand struct { 19 | } 20 | 21 | func (rc *SimpleCommand) Timeout() time.Duration { return 1 * time.Second } 22 | func (rc *SimpleCommand) Run() (interface{}, error) { 23 | return "ok", nil 24 | } 25 | 26 | func benchmarkRunN(b *testing.B) { 27 | for n := 0; n < b.N; n++ { 28 | command.Run() 29 | } 30 | } 31 | 32 | func benchmarkExecuteN(b *testing.B) { 33 | for n := 0; n < b.N; n++ { 34 | command.Execute() 35 | } 36 | } 37 | 38 | func BenchmarkRun(b *testing.B) { benchmarkRunN(b) } 39 | func BenchmarkExecute(b *testing.B) { benchmarkExecuteN(b) } 40 | -------------------------------------------------------------------------------- /statsd/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Peter Bourgon, SoundCloud Ltd. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /sample/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Richard Crowley. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY RICHARD CROWLEY ``AS IS'' AND ANY EXPRESS 16 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL RICHARD CROWLEY OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 25 | THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | The views and conclusions contained in the software and documentation 28 | are those of the authors and should not be interpreted as representing 29 | official policies, either expressed or implied, of Richard Crowley. 30 | -------------------------------------------------------------------------------- /readme_test.go: -------------------------------------------------------------------------------- 1 | package goHystrix 2 | 3 | //import "github.com/dahernan/goHystrix" 4 | import ( 5 | "fmt" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type MyStringCommand struct { 11 | message string 12 | } 13 | 14 | func (c *MyStringCommand) Run() (interface{}, error) { 15 | return c.message, nil 16 | } 17 | 18 | func (c *MyStringCommand) Fallback() (interface{}, error) { 19 | return "FALLBACK", nil 20 | } 21 | 22 | func TestString(t *testing.T) { 23 | 24 | // Sync execution 25 | command := NewCommand("stringMessage", "stringGroup", &MyStringCommand{"helloooooooo"}) 26 | value, err := command.Execute() 27 | 28 | fmt.Println("Sync call ---- ") 29 | fmt.Println("Value: ", value) 30 | fmt.Println("Error: ", err) 31 | 32 | // Async execution 33 | 34 | valueChan, errorChan := command.Queue() 35 | 36 | fmt.Println("Async call ---- ") 37 | select { 38 | case value = <-valueChan: 39 | fmt.Println("Value: ", value) 40 | case err = <-errorChan: 41 | fmt.Println("Error: ", err) 42 | } 43 | 44 | fmt.Println("Succesfull Calls ", command.HealthCounts().Success) 45 | fmt.Println("Mean: ", command.Metric().Stats().Mean()) 46 | 47 | fmt.Println("All the circuits in JSON format: ", Circuits().ToJSON()) 48 | 49 | } 50 | 51 | func TestStringWithOptions(t *testing.T) { 52 | 53 | // Sync execution 54 | command := NewCommandWithOptions("stringMessage", "stringGroup", &MyStringCommand{"helloooooooo"}, CommandOptions{ 55 | ErrorsThreshold: 60.0, 56 | MinimumNumberOfRequest: 3, 57 | NumberOfSecondsToStore: 5, 58 | NumberOfSamplesToStore: 10, 59 | Timeout: 3 * time.Second, 60 | }) 61 | value, err := command.Execute() 62 | 63 | fmt.Println("Sync call ---- ") 64 | fmt.Println("Value: ", value) 65 | fmt.Println("Error: ", err) 66 | 67 | // Async execution 68 | 69 | valueChan, errorChan := command.Queue() 70 | 71 | fmt.Println("Async call ---- ") 72 | select { 73 | case value = <-valueChan: 74 | fmt.Println("Value: ", value) 75 | case err = <-errorChan: 76 | fmt.Println("Error: ", err) 77 | } 78 | 79 | fmt.Println("Succesfull Calls ", command.HealthCounts().Success) 80 | fmt.Println("Mean: ", command.Metric().Stats().Mean()) 81 | 82 | fmt.Println("All the circuits in JSON format: ", Circuits().ToJSON()) 83 | 84 | } 85 | -------------------------------------------------------------------------------- /circuit_test.go: -------------------------------------------------------------------------------- 1 | package goHystrix 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/smartystreets/goconvey/convey" 6 | "testing" 7 | ) 8 | 9 | func TestCircuitsHolder(t *testing.T) { 10 | Convey("Circuits Holder simple Get", t, func() { 11 | circuits := NewCircuitsHolder() 12 | Convey("When Get is called in an empty map", func() { 13 | value, ok := circuits.Get("testGroup", "testKey") 14 | Convey("The result should be nil", func() { 15 | So(value, ShouldBeNil) 16 | }) 17 | Convey("The second returned value should be false", func() { 18 | So(ok, ShouldBeFalse) 19 | }) 20 | 21 | }) 22 | }) 23 | 24 | Convey("Set and Get logic", t, func() { 25 | circuits := NewCircuitsHolder() 26 | Convey("When the circuits is filled with multiple values", func() { 27 | 28 | m1 := &CircuitBreaker{name: "1"} 29 | m2 := &CircuitBreaker{name: "2"} 30 | m3 := &CircuitBreaker{name: "3"} 31 | m4 := &CircuitBreaker{name: "4"} 32 | 33 | circuits.Set("testGroup1", "testKey1", m1) 34 | circuits.Set("testGroup1", "testKey2", m2) 35 | 36 | circuits.Set("testGroup2", "testKey1", m3) 37 | circuits.Set("testGroup2", "testKey2", m4) 38 | 39 | Convey("Get return that values back", func() { 40 | value, ok := circuits.Get("testGroup1", "testKey1") 41 | So(value, ShouldEqual, m1) 42 | So(ok, ShouldBeTrue) 43 | 44 | value, ok = circuits.Get("testGroup1", "testKey2") 45 | So(value, ShouldEqual, m2) 46 | So(ok, ShouldBeTrue) 47 | 48 | value, ok = circuits.Get("testGroup2", "testKey1") 49 | So(value, ShouldEqual, m3) 50 | So(ok, ShouldBeTrue) 51 | 52 | value, ok = circuits.Get("testGroup2", "testKey2") 53 | So(value, ShouldEqual, m4) 54 | So(ok, ShouldBeTrue) 55 | 56 | }) 57 | 58 | }) 59 | }) 60 | 61 | } 62 | 63 | func TestNewCircuit(t *testing.T) { 64 | Convey("New Circuit register itself into the Circuit Holder", t, func() { 65 | NewCircuitsHolder() 66 | 67 | m1 := NewCircuitNoParams("testGroup1", "testKey1") 68 | m2 := NewCircuitNoParams("testGroup1", "testKey2") 69 | m3 := NewCircuitNoParams("testGroup2", "testKey1") 70 | m4 := NewCircuitNoParams("testGroup2", "testKey2") 71 | 72 | fmt.Println(Circuits()) 73 | fmt.Println(Circuits().ToJSON()) 74 | 75 | value, ok := Circuits().Get("testGroup1", "testKey1") 76 | So(value, ShouldEqual, m1) 77 | So(ok, ShouldBeTrue) 78 | 79 | value, ok = Circuits().Get("testGroup1", "testKey2") 80 | So(value, ShouldEqual, m2) 81 | So(ok, ShouldBeTrue) 82 | 83 | value, ok = Circuits().Get("testGroup2", "testKey1") 84 | So(value, ShouldEqual, m3) 85 | So(ok, ShouldBeTrue) 86 | 87 | value, ok = Circuits().Get("testGroup2", "testKey2") 88 | So(value, ShouldEqual, m4) 89 | So(ok, ShouldBeTrue) 90 | 91 | fmt.Println("########################") 92 | fmt.Println(Circuits().ToJSON()) 93 | 94 | }) 95 | 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Implementation in Go (aka golang) of some of the stability patterns, like Circuit Breaker. 2 | 3 | * Inspired by Nexflix Hystrix https://github.com/Netflix/Hystrix 4 | * And the book Release it! http://www.amazon.co.uk/Release-Production-Ready-Software-Pragmatic-Programmers-ebook/dp/B00A32NXZO/ 5 | 6 | You can also check [breaker](https://github.com/dahernan/breaker) my other implementaion of the Circuit Breaker, simpler and with not dependencies: https://github.com/dahernan/breaker 7 | 8 | 9 | How to use 10 | ---------- 11 | 12 | ### You need to implement goHystrix.Interface 13 | 14 | ```go 15 | type Interface interface { 16 | Run() (interface{}, error) 17 | } 18 | ``` 19 | 20 | There is also a `goHystrix.FallbackInterface`, if you need a fallback function: 21 | 22 | ```go 23 | type FallbackInterface interface { 24 | Interface 25 | Fallback() (interface{}, error) 26 | } 27 | ``` 28 | 29 | ### Basic command with a String 30 | ```go 31 | import ( 32 | "fmt" 33 | "github.com/dahernan/goHystrix" 34 | "testing" 35 | "time" 36 | ) 37 | 38 | type MyStringCommand struct { 39 | message string 40 | } 41 | 42 | // This is the normal method to execute (circuit is close) 43 | func (c *MyStringCommand) Run() (interface{}, error) { 44 | return c.message, nil 45 | } 46 | 47 | // This is the method to execute in case the circuit is open 48 | func (c *MyStringCommand) Fallback() (interface{}, error) { 49 | return "FALLBACK", nil 50 | } 51 | 52 | func TestString(t *testing.T) { 53 | // creates a new command 54 | command := goHystrix.NewCommand("commandName", "commandGroup", &MyStringCommand{"helloooooooo"}) 55 | 56 | // Sync execution 57 | value, err := command.Execute() 58 | 59 | fmt.Println("Sync call ---- ") 60 | fmt.Println("Value: ", value) 61 | fmt.Println("Error: ", err) 62 | 63 | // Async execution 64 | 65 | valueChan, errorChan := command.Queue() 66 | 67 | fmt.Println("Async call ---- ") 68 | select { 69 | case value = <-valueChan: 70 | fmt.Println("Value: ", value) 71 | case err = <-errorChan: 72 | fmt.Println("Error: ", err) 73 | } 74 | 75 | fmt.Println("Succesfull Calls ", command.HealthCounts().Success) 76 | fmt.Println("Mean: ", command.Stats().Mean()) 77 | 78 | fmt.Println("All the circuits in JSON format: ", goHystrix.Circuits().ToJSON()) 79 | 80 | 81 | } 82 | 83 | ``` 84 | 85 | ### Default circuit values when you create a command 86 | ``` 87 | ErrorPercetageThreshold - 50.0 - If (number_of_errors / total_calls * 100) > 50.0 the circuit will be open 88 | MinimumNumberOfRequest - if total_calls < 20 the circuit will be close 89 | NumberOfSecondsToStore - 20 seconds (for health counts you only evaluate the last 20 seconds of calls) 90 | NumberOfSamplesToStore - 50 values (you store the duration of 50 successful calls using reservoir sampling) 91 | Timeout - 2 * time.Seconds 92 | ``` 93 | 94 | ### You can customize the default values when you create the command 95 | ```go 96 | 97 | // ErrorPercetageThreshold - 60.0 98 | // MinimumNumberOfRequest - 3 99 | // NumberOfSecondsToStore - 5 100 | // NumberOfSamplesToStore - 10 101 | // Timeout - 10 * time.Second 102 | goHystrix.NewCommandWithOptions("commandName", "commandGroup", &MyStringCommand{"helloooooooo"}, goHystrix.CommandOptions{ 103 | ErrorsThreshold: 60.0, 104 | MinimumNumberOfRequest: 3, 105 | NumberOfSecondsToStore: 5, 106 | NumberOfSamplesToStore: 10, 107 | Timeout: 10 * time.Second, 108 | }) 109 | 110 | ``` 111 | 112 | ### Exposes all circuits information by http in JSON format 113 | ```go 114 | import _ "github.com/dahernan/goHystrix/httpexp" 115 | ``` 116 | GET - http://host/debug/circuits 117 | 118 | 119 | ### Exposes the metrics using statds 120 | 121 | ```go 122 | // host and prefix for statds server, and send the gauges of the state of the circuits every 3 Seconds 123 | goHystrix.UseStatsd("0.0.0.0:8125", "myprefix", 3*time.Second) 124 | ``` 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /export.go: -------------------------------------------------------------------------------- 1 | package goHystrix 2 | 3 | import ( 4 | "fmt" 5 | "github.com/dahernan/goHystrix/statsd" 6 | "log" 7 | "time" 8 | ) 9 | 10 | var ( 11 | metricsExporter MetricExport 12 | ) 13 | 14 | func init() { 15 | metricsExporter = NewNilExport() 16 | } 17 | 18 | func Exporter() MetricExport { 19 | return metricsExporter 20 | } 21 | 22 | func SetExporter(export MetricExport) { 23 | metricsExporter = export 24 | } 25 | 26 | type MetricExport interface { 27 | Success(group string, name string, duration time.Duration) 28 | Fail(group string, name string) 29 | Fallback(group string, name string) 30 | FallbackError(group string, name string) 31 | Timeout(group string, name string) 32 | Panic(group string, name string) 33 | State(circuits *CircuitHolder) 34 | } 35 | 36 | type StatsdExport struct { 37 | statsdClient statsd.Statter 38 | prefix string 39 | } 40 | 41 | type NilExport struct { 42 | } 43 | 44 | func UseStatsd(address string, prefix string, dur time.Duration) { 45 | statsdClient, err := statsd.Dial("udp", address) 46 | if err != nil { 47 | log.Println("Error setting Statds for publishing the metrics: ", err) 48 | log.Println("Using NilExport for publishing the metrics") 49 | SetExporter(NilExport{}) 50 | return 51 | } 52 | export := NewStatsdExport(statsdClient, prefix) 53 | SetExporter(export) 54 | 55 | statsdExport := export.(StatsdExport) 56 | // poll the state of the circuits 57 | go statsdExport.run(Circuits(), dur) 58 | } 59 | 60 | func NewNilExport() MetricExport { return NilExport{} } 61 | 62 | func (NilExport) Success(group string, name string, duration time.Duration) {} 63 | func (NilExport) Fail(group string, name string) {} 64 | func (NilExport) Fallback(group string, name string) {} 65 | func (NilExport) FallbackError(group string, name string) {} 66 | func (NilExport) Timeout(group string, name string) {} 67 | func (NilExport) Panic(group string, name string) {} 68 | func (NilExport) State(circuits *CircuitHolder) {} 69 | 70 | func NewStatsdExport(statsdClient statsd.Statter, prefix string) MetricExport { 71 | return StatsdExport{statsdClient, prefix} 72 | } 73 | 74 | func (s StatsdExport) Success(group string, name string, duration time.Duration) { 75 | go func() { 76 | s.statsdClient.Counter(1.0, fmt.Sprintf("%s.%s.%s.success", s.prefix, group, name), 1) 77 | //ms := int64(duration / time.Millisecond) 78 | s.statsdClient.Timing(1.0, fmt.Sprintf("%s.%s.%s.duration", s.prefix, group, name), duration) 79 | }() 80 | } 81 | 82 | func (s StatsdExport) Fail(group string, name string) { 83 | go func() { 84 | s.statsdClient.Counter(1.0, fmt.Sprintf("%s.%s.%s.fail", s.prefix, group, name), 1) 85 | }() 86 | } 87 | func (s StatsdExport) Fallback(group string, name string) { 88 | go func() { 89 | s.statsdClient.Counter(1.0, fmt.Sprintf("%s.%s.%s.fallback", s.prefix, group, name), 1) 90 | }() 91 | } 92 | func (s StatsdExport) FallbackError(group string, name string) { 93 | go func() { 94 | s.statsdClient.Counter(1.0, fmt.Sprintf("%s.%s.%s.fallbackError", s.prefix, group, name), 1) 95 | }() 96 | } 97 | func (s StatsdExport) Timeout(group string, name string) { 98 | go func() { 99 | s.statsdClient.Counter(1.0, fmt.Sprintf("%s.%s.%s.timeout", s.prefix, group, name), 1) 100 | }() 101 | } 102 | func (s StatsdExport) Panic(group string, name string) { 103 | go func() { 104 | s.statsdClient.Counter(1.0, fmt.Sprintf("%s.%s.%s.panic", s.prefix, group, name), 1) 105 | }() 106 | } 107 | 108 | func (s StatsdExport) State(holder *CircuitHolder) { 109 | // TODO: have a save way to iterate over the circuits without 110 | // knowing how is implemented 111 | holder.mutex.RLock() 112 | defer holder.mutex.RUnlock() 113 | for group, names := range holder.circuits { 114 | for name, circuit := range names { 115 | var state string 116 | open, _ := circuit.IsOpen() 117 | state = "0" 118 | if open { 119 | state = "1" 120 | } 121 | s.statsdClient.Gauge(1.0, fmt.Sprintf("%s.%s.%s.open", s.prefix, group, name), state) 122 | } 123 | } 124 | } 125 | 126 | func (s StatsdExport) run(holder *CircuitHolder, dur time.Duration) { 127 | for { 128 | select { 129 | case <-time.After(dur): 130 | s.State(holder) 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /metrics_test.go: -------------------------------------------------------------------------------- 1 | package goHystrix 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | //"fmt" 7 | . "github.com/smartystreets/goconvey/convey" 8 | "testing" 9 | //"time" 10 | ) 11 | 12 | func TestCountersSuccess(t *testing.T) { 13 | Convey("Metric stores the success counter", t, func() { 14 | metric := NewMetric("testGroup", "testName") 15 | 16 | metric.Success(1) 17 | metric.Success(2) 18 | 19 | c1 := metric.HealthCounts() 20 | 21 | s := c1.Success 22 | 23 | So(s, ShouldEqual, 2) 24 | So(c1.Failures, ShouldEqual, 0) 25 | So(c1.Timeouts, ShouldEqual, 0) 26 | So(c1.Fallback, ShouldEqual, 0) 27 | So(c1.FallbackErrors, ShouldEqual, 0) 28 | 29 | metric.Success(3) 30 | 31 | c2 := metric.HealthCounts() 32 | 33 | So(c1.Success, ShouldEqual, 2) 34 | 35 | So(c2.Success, ShouldEqual, 3) 36 | So(c2.Failures, ShouldEqual, 0) 37 | So(c2.Timeouts, ShouldEqual, 0) 38 | So(c2.Fallback, ShouldEqual, 0) 39 | So(c2.FallbackErrors, ShouldEqual, 0) 40 | 41 | }) 42 | } 43 | 44 | func TestCounters(t *testing.T) { 45 | Convey("Metric stores the others counters", t, func() { 46 | metric := NewMetric("testGroup", "testName") 47 | 48 | metric.Success(1) 49 | metric.Success(2) 50 | metric.Success(3) 51 | metric.Success(4) 52 | metric.Fail() 53 | metric.Fail() 54 | metric.Fail() 55 | metric.Fallback() 56 | metric.Fallback() 57 | metric.FallbackError() 58 | metric.FallbackError() 59 | metric.FallbackError() 60 | metric.Timeout() 61 | metric.Timeout() 62 | metric.Timeout() 63 | metric.Timeout() 64 | 65 | c1 := metric.HealthCounts() 66 | 67 | So(c1.Success, ShouldEqual, 4) 68 | So(c1.Failures, ShouldEqual, 7) 69 | So(c1.Timeouts, ShouldEqual, 4) 70 | So(c1.Fallback, ShouldEqual, 2) 71 | So(c1.FallbackErrors, ShouldEqual, 3) 72 | So(c1.Total, ShouldEqual, 11) 73 | So(c1.ErrorPercentage, ShouldEqual, 63.63636363636363) 74 | 75 | metric.Fail() 76 | metric.Success(5) 77 | metric.Fallback() 78 | metric.FallbackError() 79 | metric.Timeout() 80 | 81 | c2 := metric.HealthCounts() 82 | 83 | So(c2.Success, ShouldEqual, 5) 84 | So(c2.Failures, ShouldEqual, 9) 85 | So(c2.Timeouts, ShouldEqual, 5) 86 | So(c2.Fallback, ShouldEqual, 3) 87 | So(c2.FallbackErrors, ShouldEqual, 4) 88 | So(c2.Total, ShouldEqual, 14) 89 | So(c2.ErrorPercentage, ShouldEqual, 64.28571428571429) 90 | 91 | }) 92 | } 93 | 94 | func TestRollingsCounters(t *testing.T) { 95 | Convey("Metric stores the counters in buckets for rolling the counters", t, func() { 96 | metric := NewMetricWithParams("group", "name", 4, 10) 97 | fmt.Println("== metric.Success(1)") 98 | metric.Success(1) 99 | metric.Success(1) 100 | metric.Success(1) 101 | metric.Fail() 102 | metric.Fallback() 103 | metric.FallbackError() 104 | metric.Timeout() 105 | time.Sleep(3 * time.Second) 106 | 107 | fmt.Println("== metric.Success(2)") 108 | metric.Success(2) 109 | metric.Success(2) 110 | time.Sleep(1 * time.Second) 111 | fmt.Println("== metric.Success(3)") 112 | metric.Fail() 113 | metric.Fail() 114 | metric.Fallback() 115 | metric.FallbackError() 116 | metric.Timeout() 117 | metric.Panic() 118 | 119 | metric.Success(3) 120 | time.Sleep(1 * time.Second) 121 | fmt.Println("== metric.Success(4)") 122 | metric.Success(4) 123 | metric.Fail() 124 | metric.Fail() 125 | metric.Fail() 126 | metric.Fallback() 127 | metric.FallbackError() 128 | metric.Timeout() 129 | metric.Panic() 130 | 131 | c1 := metric.HealthCounts() 132 | 133 | fmt.Println("== c1", c1) 134 | 135 | So(c1.Success, ShouldEqual, 4) 136 | So(c1.Failures, ShouldEqual, 7) 137 | So(c1.Timeouts, ShouldEqual, 2) 138 | So(c1.Fallback, ShouldEqual, 2) 139 | So(c1.FallbackErrors, ShouldEqual, 2) 140 | So(c1.Panics, ShouldEqual, 2) 141 | So(c1.Total, ShouldEqual, 11) 142 | So(c1.ErrorPercentage, ShouldEqual, 63.63636363636363) 143 | 144 | }) 145 | } 146 | 147 | func TestMetricsKeepMessuaresSample(t *testing.T) { 148 | Convey("Keep the stats of the duration for the successful results", t, func() { 149 | metric := NewMetricWithParams("group", "name", 4, 20) 150 | metric.Success(5) 151 | metric.Success(1) 152 | metric.Success(9) 153 | metric.Success(2) 154 | metric.Success(5) 155 | metric.Success(8) 156 | 157 | So(metric.Stats().Max(), ShouldEqual, 9) 158 | So(metric.Stats().Min(), ShouldEqual, 1) 159 | // flacky because is calculanting async 160 | //So(metric.Stats().Mean(), ShouldEqual, 5) 161 | //So(metric.Stats().Count(), ShouldEqual, 6) 162 | //So(metric.Stats().Variance(), ShouldEqual, 8.333333333333334) 163 | 164 | }) 165 | } 166 | -------------------------------------------------------------------------------- /circuit.go: -------------------------------------------------------------------------------- 1 | package goHystrix 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | type CircuitBreaker struct { 10 | name string 11 | group string 12 | 13 | metric *Metric 14 | errorsThreshold float64 15 | minRequestThreshold int64 16 | } 17 | 18 | var ( 19 | circuits = NewCircuitsHolder() 20 | ) 21 | 22 | type CircuitHolder struct { 23 | circuits map[string]map[string]*CircuitBreaker 24 | mutex sync.RWMutex 25 | } 26 | 27 | func NewCircuitNoParams(group string, name string) *CircuitBreaker { 28 | return NewCircuit(group, name, CommandOptionsDefaults()) 29 | } 30 | 31 | func NewCircuit(group string, name string, options CommandOptions) *CircuitBreaker { 32 | c, ok := Circuits().Get(group, name) 33 | if ok { 34 | return c 35 | } 36 | metric := NewMetricWithParams(group, name, options.NumberOfSecondsToStore, options.NumberOfSamplesToStore) 37 | c = &CircuitBreaker{ 38 | name: name, 39 | group: group, 40 | metric: metric, 41 | errorsThreshold: options.ErrorsThreshold, 42 | minRequestThreshold: options.MinimumNumberOfRequest, 43 | } 44 | 45 | Circuits().Set(group, name, c) 46 | return c 47 | 48 | } 49 | 50 | func (c *CircuitBreaker) IsOpen() (bool, string) { 51 | counts := c.metric.HealthCounts() 52 | 53 | if counts.Total < c.minRequestThreshold { 54 | return false, "CLOSE: not enought request" 55 | } 56 | 57 | if counts.ErrorPercentage >= c.errorsThreshold { 58 | return true, "OPEN: to many errors" 59 | } 60 | return false, "CLOSE: all ok" 61 | } 62 | 63 | func (c *CircuitBreaker) Metric() *Metric { 64 | return c.metric 65 | } 66 | 67 | func (c *CircuitBreaker) ToJSON() string { 68 | 69 | var buffer bytes.Buffer 70 | 71 | buffer.WriteString("{\n") 72 | 73 | open, state := c.IsOpen() 74 | counts := c.Metric().doHealthCounts() 75 | stats := c.Metric().Stats() 76 | lastSuccess := c.Metric().LastSuccess() 77 | lastFailure := c.Metric().LastFailure() 78 | lastTimeout := c.Metric().LastTimeout() 79 | 80 | fmt.Fprintf(&buffer, "\"name\" : \"%s\",\n", c.name) 81 | fmt.Fprintf(&buffer, "\"group\" : \"%s\",\n", c.group) 82 | 83 | fmt.Fprintf(&buffer, "\"isOpen\" : \"%t\",\n", open) 84 | fmt.Fprintf(&buffer, "\"state\" : \"%s\",\n", state) 85 | 86 | fmt.Fprintf(&buffer, "\"percentile90\" : \"%f\",\n", stats.Percentile(0.90)) 87 | fmt.Fprintf(&buffer, "\"mean\" : \"%f\",\n", stats.Mean()) 88 | fmt.Fprintf(&buffer, "\"variance\" : \"%f\",\n", stats.Variance()) 89 | 90 | fmt.Fprintf(&buffer, "\"max\" : \"%d\",\n", stats.Max()) 91 | fmt.Fprintf(&buffer, "\"min\" : \"%d\",\n", stats.Min()) 92 | 93 | fmt.Fprintf(&buffer, "\"failures\" : \"%d\",\n", counts.Failures) 94 | fmt.Fprintf(&buffer, "\"timeouts\" : \"%d\",\n", counts.Timeouts) 95 | fmt.Fprintf(&buffer, "\"fallback\" : \"%d\",\n", counts.Fallback) 96 | fmt.Fprintf(&buffer, "\"panics\" : \"%d\",\n", counts.Panics) 97 | fmt.Fprintf(&buffer, "\"fallbackErrors\" : \"%d\",\n", counts.FallbackErrors) 98 | fmt.Fprintf(&buffer, "\"total\" : \"%d\",\n", counts.Total) 99 | fmt.Fprintf(&buffer, "\"success\" : \"%d\",\n", counts.Success) 100 | fmt.Fprintf(&buffer, "\"errorPercentage\" : \"%f\",\n", counts.ErrorPercentage) 101 | 102 | fmt.Fprintf(&buffer, "\"lastSuccess\" : \"%s\",\n", lastSuccess) 103 | fmt.Fprintf(&buffer, "\"lastFailure\" : \"%s\",\n", lastFailure) 104 | fmt.Fprintf(&buffer, "\"lastTimeout\" : \"%s\"\n", lastTimeout) 105 | 106 | buffer.WriteString("\n}") 107 | 108 | return buffer.String() 109 | 110 | } 111 | 112 | func NewCircuitsHolder() *CircuitHolder { 113 | return &CircuitHolder{circuits: make(map[string]map[string]*CircuitBreaker)} 114 | } 115 | 116 | func Circuits() *CircuitHolder { 117 | return circuits 118 | } 119 | func CircuitsReset() { 120 | circuits = NewCircuitsHolder() 121 | } 122 | 123 | func (holder *CircuitHolder) Get(group string, name string) (*CircuitBreaker, bool) { 124 | holder.mutex.RLock() 125 | defer holder.mutex.RUnlock() 126 | circuitsValues, ok := holder.circuits[group] 127 | if !ok { 128 | return nil, ok 129 | } 130 | 131 | value, ok := circuitsValues[name] 132 | return value, ok 133 | } 134 | 135 | func (holder *CircuitHolder) Set(group string, name string, value *CircuitBreaker) { 136 | holder.mutex.Lock() 137 | defer holder.mutex.Unlock() 138 | 139 | circuitsValues, ok := holder.circuits[group] 140 | if !ok { 141 | circuitsValues = make(map[string]*CircuitBreaker) 142 | holder.circuits[group] = circuitsValues 143 | } 144 | circuitsValues[name] = value 145 | } 146 | 147 | func (holder *CircuitHolder) ToJSON() string { 148 | holder.mutex.RLock() 149 | defer holder.mutex.RUnlock() 150 | 151 | var buffer bytes.Buffer 152 | 153 | buffer.WriteString("[\n") 154 | 155 | first := true 156 | for group, names := range holder.circuits { 157 | if !first { 158 | fmt.Fprintf(&buffer, ",\n") 159 | } 160 | first = false 161 | nested_first := true 162 | fmt.Fprintf(&buffer, "{\"group\" : \"%s\",\n", group) 163 | fmt.Fprintf(&buffer, "\"circuit\" : [\n") 164 | for _, circuit := range names { 165 | if !nested_first { 166 | fmt.Fprintf(&buffer, ",\n") 167 | } 168 | nested_first = false 169 | buffer.WriteString(circuit.ToJSON()) 170 | } 171 | fmt.Fprintf(&buffer, "] }\n") 172 | } 173 | 174 | buffer.WriteString("\n]") 175 | return buffer.String() 176 | } 177 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | package goHystrix 2 | 3 | import ( 4 | "github.com/dahernan/goHystrix/sample" 5 | "time" 6 | ) 7 | 8 | const ( 9 | alpha = 0.015 // alpha for the exponential decay distribution 10 | ) 11 | 12 | type Metric struct { 13 | name string 14 | group string 15 | 16 | successChan chan time.Duration 17 | failuresChan chan struct{} 18 | fallbackChan chan struct{} 19 | fallbackErrorChan chan struct{} 20 | timeoutsChan chan struct{} 21 | panicChan chan struct{} 22 | countersChan chan struct{} 23 | countersOutChan chan HealthCounts 24 | 25 | buckets int 26 | window time.Duration 27 | values []HealthCountsBucket 28 | 29 | sample sample.Sample 30 | 31 | lastFailure time.Time 32 | lastSuccess time.Time 33 | lastTimeout time.Time 34 | } 35 | 36 | func NewMetric(group string, name string) *Metric { 37 | return NewMetricWithParams(group, name, 20, 50) 38 | } 39 | 40 | func NewMetricWithParams(group string, name string, numberOfSecondsToStore int, sampleSize int) *Metric { 41 | m := &Metric{} 42 | m.name = name 43 | m.group = group 44 | m.buckets = numberOfSecondsToStore 45 | m.window = time.Duration(numberOfSecondsToStore) * time.Second 46 | m.values = make([]HealthCountsBucket, m.buckets, m.buckets) 47 | 48 | m.sample = sample.NewExpDecaySample(sampleSize, alpha) 49 | 50 | m.successChan = make(chan time.Duration) 51 | m.failuresChan = make(chan struct{}) 52 | m.fallbackChan = make(chan struct{}) 53 | m.fallbackErrorChan = make(chan struct{}) 54 | m.timeoutsChan = make(chan struct{}) 55 | m.panicChan = make(chan struct{}) 56 | m.countersChan = make(chan struct{}) 57 | m.countersOutChan = make(chan HealthCounts) 58 | 59 | go m.run() 60 | return m 61 | 62 | } 63 | 64 | type HealthCountsBucket struct { 65 | Failures int64 66 | Success int64 67 | Fallback int64 68 | FallbackErrors int64 69 | Timeouts int64 70 | Panics int64 71 | lastWrite time.Time 72 | } 73 | 74 | type HealthCounts struct { 75 | HealthCountsBucket 76 | Total int64 77 | ErrorPercentage float64 78 | } 79 | 80 | func (c *HealthCountsBucket) Reset() { 81 | c.Failures = 0 82 | c.Success = 0 83 | c.Fallback = 0 84 | c.FallbackErrors = 0 85 | c.Timeouts = 0 86 | c.Panics = 0 87 | } 88 | 89 | func (m *Metric) run() { 90 | for { 91 | select { 92 | case duration := <-m.successChan: 93 | m.doSuccess(duration) 94 | case <-m.failuresChan: 95 | m.doFail() 96 | case <-m.timeoutsChan: 97 | m.doTimeout() 98 | case <-m.fallbackChan: 99 | m.doFallback() 100 | case <-m.fallbackErrorChan: 101 | m.doFallbackError() 102 | case <-m.panicChan: 103 | m.doPanic() 104 | case <-m.countersChan: 105 | m.countersOutChan <- m.doHealthCounts() 106 | //case <-time.After(2 * time.Second): 107 | // fmt.Println("NOTHING :(") 108 | } 109 | } 110 | 111 | } 112 | 113 | func (m *Metric) bucket() *HealthCountsBucket { 114 | now := time.Now() 115 | index := now.Second() % m.buckets 116 | if !m.values[index].lastWrite.IsZero() { 117 | elapsed := now.Sub(m.values[index].lastWrite) 118 | if elapsed > m.window { 119 | m.values[index].Reset() 120 | } 121 | } 122 | m.values[index].lastWrite = now 123 | return &m.values[index] 124 | } 125 | 126 | func (m *Metric) doSuccess(duration time.Duration) { 127 | m.bucket().Success++ 128 | m.lastSuccess = time.Now() 129 | 130 | go func(d time.Duration) { 131 | m.sample.Update(int64(d)) 132 | }(duration) 133 | 134 | Exporter().Success(m.group, m.name, duration) 135 | } 136 | 137 | func (m *Metric) doFail() { 138 | m.bucket().Failures++ 139 | m.lastFailure = time.Now() 140 | Exporter().Fail(m.group, m.name) 141 | } 142 | 143 | func (m *Metric) doFallback() { 144 | m.bucket().Fallback++ 145 | Exporter().Fallback(m.group, m.name) 146 | } 147 | 148 | func (m *Metric) doTimeout() { 149 | m.bucket().Timeouts++ 150 | m.bucket().Failures++ 151 | now := time.Now() 152 | m.lastFailure = now 153 | m.lastTimeout = now 154 | Exporter().Timeout(m.group, m.name) 155 | } 156 | 157 | func (m *Metric) doFallbackError() { 158 | m.bucket().FallbackErrors++ 159 | Exporter().FallbackError(m.group, m.name) 160 | } 161 | 162 | func (m *Metric) doPanic() { 163 | m.bucket().Panics++ 164 | Exporter().Panic(m.group, m.name) 165 | } 166 | 167 | func (m *Metric) doHealthCounts() (counters HealthCounts) { 168 | now := time.Now() 169 | for _, value := range m.values { 170 | if !value.lastWrite.IsZero() && (now.Sub(value.lastWrite) <= m.window) { 171 | counters.Success += value.Success 172 | counters.Failures += value.Failures 173 | counters.Fallback += value.Fallback 174 | counters.FallbackErrors += value.FallbackErrors 175 | counters.Timeouts += value.Timeouts 176 | counters.Panics += value.Panics 177 | } 178 | } 179 | counters.Total = counters.Success + counters.Failures 180 | if counters.Total == 0 { 181 | counters.ErrorPercentage = 0 182 | } else { 183 | counters.ErrorPercentage = float64(counters.Failures) / float64(counters.Total) * 100.0 184 | } 185 | return 186 | } 187 | 188 | func (m *Metric) HealthCounts() HealthCounts { 189 | m.countersChan <- struct{}{} 190 | return <-m.countersOutChan 191 | } 192 | 193 | func (m *Metric) Success(duration time.Duration) { 194 | m.successChan <- duration 195 | } 196 | 197 | func (m *Metric) Fail() { 198 | m.failuresChan <- struct{}{} 199 | } 200 | 201 | func (m *Metric) Fallback() { 202 | m.fallbackChan <- struct{}{} 203 | } 204 | 205 | func (m *Metric) FallbackError() { 206 | m.fallbackErrorChan <- struct{}{} 207 | } 208 | 209 | func (m *Metric) Timeout() { 210 | m.timeoutsChan <- struct{}{} 211 | } 212 | 213 | func (m *Metric) Panic() { 214 | m.panicChan <- struct{}{} 215 | } 216 | 217 | func (m *Metric) Stats() sample.Sample { 218 | return m.sample 219 | } 220 | 221 | func (m *Metric) LastFailure() time.Time { 222 | return m.lastFailure 223 | } 224 | func (m *Metric) LastSuccess() time.Time { 225 | return m.lastSuccess 226 | } 227 | func (m *Metric) LastTimeout() time.Time { 228 | return m.lastTimeout 229 | } 230 | -------------------------------------------------------------------------------- /statsd/statsd.go: -------------------------------------------------------------------------------- 1 | package statsd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "math/rand" 8 | "net" 9 | "time" 10 | ) 11 | 12 | const ( 13 | MAX_PACKET_SIZE = 65536 - 8 - 20 // 8-byte UDP header, 20-byte IP header 14 | ) 15 | 16 | type Statter interface { 17 | Counter(sampleRate float32, bucket string, n ...int) 18 | Timing(sampleRate float32, bucket string, d ...time.Duration) 19 | Gauge(sampleRate float32, bucket string, value ...string) 20 | } 21 | 22 | type statsd struct { 23 | w io.Writer 24 | } 25 | 26 | // Dial takes the same parameters as net.Dial, ie. a transport protocol 27 | // (typically "udp") and an endpoint. It returns a new Statsd structure, 28 | // ready to use. 29 | // 30 | // Note that g2s currently performs no management on the connection it creates. 31 | func Dial(proto, endpoint string) (Statter, error) { 32 | c, err := net.DialTimeout(proto, endpoint, 2*time.Second) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return New(c) 37 | } 38 | 39 | // New constructs a Statsd structure which will write statsd-protocol messages 40 | // into the given io.Writer. New is intended to be used by consumers who want 41 | // nonstandard behavior: for example, they may pass an io.Writer which performs 42 | // buffering and aggregation of statsd-protocol messages. 43 | // 44 | // Note that g2s provides no synchronization. If you pass an io.Writer which 45 | // is not goroutine-safe, for example a bytes.Buffer, you must make sure you 46 | // synchronize your calls to the Statter methods. 47 | func New(w io.Writer) (Statter, error) { 48 | return &statsd{ 49 | w: w, 50 | }, nil 51 | } 52 | 53 | // bufferize folds the slice of sendables into a slice of byte-buffers, 54 | // each of which shall be no larger than max bytes. 55 | func bufferize(sendables []sendable, max int) [][]byte { 56 | bN := [][]byte{} 57 | b1, b1sz := []byte{}, 0 58 | 59 | for _, sendable := range sendables { 60 | buf := []byte(sendable.Message()) 61 | if b1sz+len(buf) > max { 62 | bN = append(bN, b1) 63 | b1, b1sz = []byte{}, 0 64 | } 65 | b1 = append(b1, buf...) 66 | b1sz += len(buf) 67 | } 68 | 69 | if len(b1) > 0 { 70 | bN = append(bN, b1[0:len(b1)-1]) 71 | } 72 | 73 | return bN 74 | } 75 | 76 | // publish folds the slice of sendables into one or more packets, each of which 77 | // will be no larger than MAX_PACKET_SIZE. It then writes them, one by one, 78 | // into the Statsd io.Writer. 79 | func (s *statsd) publish(msgs []sendable) { 80 | for _, buf := range bufferize(msgs, MAX_PACKET_SIZE) { 81 | // In the base case, when the Statsd struct is backed by a net.Conn, 82 | // "Multiple goroutines may invoke methods on a Conn simultaneously." 83 | // -- http://golang.org/pkg/net/#Conn 84 | // Otherwise, Bring Your Own Synchronization™. 85 | n, err := s.w.Write(buf) 86 | if err != nil { 87 | log.Printf("g2s: publish: %s", err) 88 | } else if n != len(buf) { 89 | log.Printf("g2s: publish: short send: %d < %d", n, len(buf)) 90 | } 91 | } 92 | } 93 | 94 | // maybeSample returns a sampling structure and true if a pseudorandom number 95 | // in the range 0..1 is less than or equal to the passed rate. 96 | // 97 | // As a special case, if r >= 1.0, maybeSample will return an uninitialized 98 | // sampling structure and true. The uninitialized sampling structure implies 99 | // enabled == false, which tells statsd that the value is unsampled. 100 | func maybeSample(r float32) (sampling, bool) { 101 | if r >= 1.0 { 102 | return sampling{}, true 103 | } 104 | 105 | if rand.Float32() > r { 106 | return sampling{}, false 107 | } 108 | 109 | return sampling{ 110 | enabled: true, 111 | rate: r, 112 | }, true 113 | } 114 | 115 | // Counter sends one or more counter statistics to statsd. 116 | // 117 | // Application code should call it for every potential invocation of a 118 | // statistic; it uses the sampleRate to determine whether or not to send or 119 | // squelch the data, on an aggregate basis. 120 | func (s *statsd) Counter(sampleRate float32, bucket string, n ...int) { 121 | samp, ok := maybeSample(sampleRate) 122 | if !ok { 123 | return 124 | } 125 | 126 | msgs := make([]sendable, len(n)) 127 | for i, ni := range n { 128 | msgs[i] = &counterUpdate{ 129 | bucket: bucket, 130 | n: ni, 131 | sampling: samp, 132 | } 133 | } 134 | 135 | s.publish(msgs) 136 | } 137 | 138 | // Timing sends one or more timing statistics to statsd. 139 | // 140 | // Application code should call it for every potential invocation of a 141 | // statistic; it uses the sampleRate to determine whether or not to send or 142 | // squelch the data, on an aggregate basis. 143 | func (s *statsd) Timing(sampleRate float32, bucket string, d ...time.Duration) { 144 | samp, ok := maybeSample(sampleRate) 145 | if !ok { 146 | return 147 | } 148 | 149 | msgs := make([]sendable, len(d)) 150 | for i, di := range d { 151 | msgs[i] = &timingUpdate{ 152 | bucket: bucket, 153 | ms: int(di.Nanoseconds() / 1e6), 154 | sampling: samp, 155 | } 156 | } 157 | 158 | s.publish(msgs) 159 | } 160 | 161 | // Gauge sends one or more gauge statistics to statsd. 162 | // 163 | // Application code should call it for every potential invocation of a 164 | // statistic; it uses the sampleRate to determine whether or not to send or 165 | // squelch the data, on an aggregate basis. 166 | func (s *statsd) Gauge(sampleRate float32, bucket string, v ...string) { 167 | samp, ok := maybeSample(sampleRate) 168 | if !ok { 169 | return 170 | } 171 | 172 | msgs := make([]sendable, len(v)) 173 | for i, vi := range v { 174 | msgs[i] = &gaugeUpdate{ 175 | bucket: bucket, 176 | val: vi, 177 | sampling: samp, 178 | } 179 | } 180 | 181 | s.publish(msgs) 182 | } 183 | 184 | type sendable interface { 185 | Message() string 186 | } 187 | 188 | type sampling struct { 189 | enabled bool 190 | rate float32 191 | } 192 | 193 | func (s *sampling) Suffix() string { 194 | if s.enabled { 195 | return fmt.Sprintf("|@%f", s.rate) 196 | } 197 | return "" 198 | } 199 | 200 | type counterUpdate struct { 201 | bucket string 202 | n int 203 | sampling 204 | } 205 | 206 | func (u *counterUpdate) Message() string { 207 | return fmt.Sprintf("%s:%d|c%s\n", u.bucket, u.n, u.sampling.Suffix()) 208 | } 209 | 210 | type timingUpdate struct { 211 | bucket string 212 | ms int 213 | sampling 214 | } 215 | 216 | func (u *timingUpdate) Message() string { 217 | return fmt.Sprintf("%s:%d|ms%s\n", u.bucket, u.ms, u.sampling.Suffix()) 218 | } 219 | 220 | type gaugeUpdate struct { 221 | bucket string 222 | val string 223 | sampling 224 | } 225 | 226 | func (u *gaugeUpdate) Message() string { 227 | return fmt.Sprintf("%s:%s|g%s\n", u.bucket, u.val, u.sampling.Suffix()) 228 | } 229 | -------------------------------------------------------------------------------- /executor.go: -------------------------------------------------------------------------------- 1 | package goHystrix 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type Interface interface { 11 | Run() (interface{}, error) 12 | } 13 | 14 | type FallbackInterface interface { 15 | Interface 16 | Fallback() (interface{}, error) 17 | } 18 | 19 | type Command struct { 20 | Interface 21 | *Executor 22 | } 23 | 24 | type Executor struct { 25 | group string 26 | name string 27 | timeout time.Duration 28 | command Interface 29 | circuit *CircuitBreaker 30 | } 31 | 32 | type CommandError struct { 33 | group string 34 | name string 35 | runError error 36 | fallbackError error 37 | } 38 | 39 | // CommandOptions, you can custimize the values, for the Circuit Breaker and the Metrics stores 40 | // ErrorsThreshold - if number_of_errors / total_calls * 100 > errorThreshold the circuit will be open 41 | // MinimumNumberOfRequest - if total_calls < minimumNumberOfRequest the circuit will be close 42 | // NumberOfSecondsToStore - Is the number of seconds to count the stats, for example 10 stores just the last 10 seconds of calls 43 | // NumberOfSamplesToStore - Is the number of samples to store for calculate the stats, greater means more precision to get Mean, Max, Min... 44 | // Timeout - the timeout for the command 45 | type CommandOptions struct { 46 | ErrorsThreshold float64 47 | MinimumNumberOfRequest int64 48 | NumberOfSecondsToStore int 49 | NumberOfSamplesToStore int 50 | Timeout time.Duration 51 | } 52 | 53 | // CommandOptionsDefaults 54 | // ErrorsThreshold - 50 - If number_of_errors / total_calls * 100 > 50.0 the circuit will be open 55 | // MinimumNumberOfRequest - if total_calls < 20 the circuit will be close 56 | // NumberOfSecondsToStore - 20 seconds 57 | // NumberOfSamplesToStore - 50 values 58 | // Timeout - 2 * time.Seconds 59 | func CommandOptionsDefaults() CommandOptions { 60 | return CommandOptions{ 61 | ErrorsThreshold: 50.0, 62 | MinimumNumberOfRequest: 20, 63 | NumberOfSecondsToStore: 20, 64 | NumberOfSamplesToStore: 20, 65 | Timeout: 2 * time.Second, 66 | } 67 | 68 | } 69 | 70 | // NewCommand- create a new command with the default values 71 | func NewCommand(name string, group string, command Interface) *Command { 72 | executor := NewExecutor(name, group, command, CommandOptionsDefaults()) 73 | return &Command{Interface: command, Executor: executor} 74 | } 75 | 76 | func NewCommandWithOptions(name string, group string, command Interface, options CommandOptions) *Command { 77 | executor := NewExecutor(name, group, command, options) 78 | return &Command{Interface: command, Executor: executor} 79 | } 80 | 81 | func NewExecutor(name string, group string, command Interface, options CommandOptions) *Executor { 82 | circuit := NewCircuit(group, name, options) 83 | return &Executor{ 84 | group: group, 85 | name: name, 86 | timeout: options.Timeout, 87 | command: command, 88 | circuit: circuit, 89 | } 90 | } 91 | 92 | func (ex *Executor) doExecute() (interface{}, error) { 93 | valueChan := make(chan interface{}, 1) 94 | errorChan := make(chan error, 1) 95 | var elapsed time.Duration 96 | go func() { 97 | defer func() { 98 | if r := recover(); r != nil { 99 | ex.Metric().Panic() 100 | errorChan <- fmt.Errorf("Recovered from panic: %v", r) 101 | } 102 | }() 103 | start := time.Now() 104 | value, err := ex.command.Run() 105 | elapsed = time.Since(start) 106 | if err != nil { 107 | errorChan <- err 108 | } else { 109 | valueChan <- value 110 | } 111 | }() 112 | 113 | select { 114 | case value := <-valueChan: 115 | ex.Metric().Success(elapsed) 116 | return value, nil 117 | case err := <-errorChan: 118 | ex.Metric().Fail() 119 | return nil, err 120 | case <-time.After(ex.timeout): 121 | ex.Metric().Timeout() 122 | return nil, fmt.Errorf("error: Timeout (%s), executing command %s:%s", ex.timeout, ex.group, ex.name) 123 | } 124 | 125 | } 126 | 127 | func (ex *Executor) doFallback(nestedError error) (interface{}, error) { 128 | ex.Metric().Fallback() 129 | 130 | fbCmd, ok := ex.command.(FallbackInterface) 131 | if !ok { 132 | ex.Metric().FallbackError() 133 | return nil, NewCommandError(ex.group, ex.name, nestedError, fmt.Errorf("No fallback implementation available for %s", ex.name)) 134 | } 135 | 136 | value, err := fbCmd.Fallback() 137 | if err != nil { 138 | ex.Metric().FallbackError() 139 | return value, NewCommandError(ex.group, ex.name, nestedError, err) 140 | } 141 | 142 | // log the nested error 143 | if nestedError != nil { 144 | commandError := NewCommandError(ex.group, ex.name, nestedError, nil) 145 | log.Println(commandError.Error()) 146 | } 147 | 148 | return value, err 149 | 150 | } 151 | 152 | func (ex *Executor) Execute() (interface{}, error) { 153 | open, _ := ex.circuit.IsOpen() 154 | if open { 155 | return ex.doFallback(nil) 156 | } 157 | 158 | value, err := ex.doExecute() 159 | if err != nil { 160 | return ex.doFallback(err) 161 | } 162 | return value, err 163 | 164 | } 165 | 166 | func (ex *Executor) Queue() (chan interface{}, chan error) { 167 | valueChan := make(chan interface{}, 1) 168 | errorChan := make(chan error, 1) 169 | 170 | go func() { 171 | value, err := ex.Execute() 172 | if value != nil { 173 | valueChan <- value 174 | } 175 | if err != nil { 176 | errorChan <- err 177 | } 178 | }() 179 | return valueChan, errorChan 180 | } 181 | 182 | func (ex *Executor) Metric() *Metric { 183 | return ex.circuit.Metric() 184 | } 185 | 186 | func (ex *Executor) HealthCounts() HealthCounts { 187 | return ex.Metric().HealthCounts() 188 | } 189 | 190 | // Nested error handling 191 | func (e CommandError) Error() string { 192 | runErrorText := "" 193 | fallbackErrorText := "" 194 | commandText := fmt.Sprintf("[%s:%s]", e.group, e.name) 195 | if e.runError != nil { 196 | runErrorText = fmt.Sprintf("RunError: %s", e.runError.Error()) 197 | 198 | } 199 | if e.fallbackError != nil { 200 | fallbackErrorText = fmt.Sprintf("FallbackError: %s", e.fallbackError.Error()) 201 | } 202 | 203 | return strings.TrimSpace(fmt.Sprintf("%s %s %s", commandText, fallbackErrorText, runErrorText)) 204 | } 205 | 206 | func NewCommandError(group string, name string, runError error, fallbackError error) CommandError { 207 | return CommandError{group, name, runError, fallbackError} 208 | } 209 | 210 | // Same API but with Funcional flavor 211 | type CommandFunc func() (interface{}, error) 212 | type CommandFuncWrap struct { 213 | run CommandFunc 214 | } 215 | 216 | func (c CommandFuncWrap) Run() (interface{}, error) { 217 | return c.run() 218 | } 219 | 220 | type CommandFuncFallbackWrap struct { 221 | run CommandFunc 222 | fallback CommandFunc 223 | } 224 | 225 | func (c CommandFuncFallbackWrap) Fallback() (interface{}, error) { 226 | return c.fallback() 227 | } 228 | 229 | func (c CommandFuncFallbackWrap) Run() (interface{}, error) { 230 | return c.run() 231 | } 232 | 233 | func NewCommandFunc(name string, group string, commandFunc CommandFunc) *Command { 234 | command := CommandFuncWrap{commandFunc} 235 | executor := NewExecutor(name, group, command, CommandOptionsDefaults()) 236 | return &Command{Interface: command, Executor: executor} 237 | } 238 | func NewCommandFuncFallback(name string, group string, commandFunc CommandFunc, fallbackFunc CommandFunc) *Command { 239 | command := CommandFuncFallbackWrap{ 240 | run: commandFunc, 241 | fallback: fallbackFunc, 242 | } 243 | executor := NewExecutor(name, group, command, CommandOptionsDefaults()) 244 | return &Command{Interface: command, Executor: executor} 245 | } 246 | -------------------------------------------------------------------------------- /sample/sample_test.go: -------------------------------------------------------------------------------- 1 | package sample 2 | 3 | import ( 4 | "math/rand" 5 | "runtime" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // Benchmark{Compute,Copy}{1000,1000000} demonstrate that, even for relatively 11 | // expensive computations like Variance, the cost of copying the Sample, as 12 | // approximated by a make and copy, is much greater than the cost of the 13 | // computation for small samples and only slightly less for large samples. 14 | func BenchmarkCompute1000(b *testing.B) { 15 | s := make([]int64, 1000) 16 | for i := 0; i < len(s); i++ { 17 | s[i] = int64(i) 18 | } 19 | b.ResetTimer() 20 | for i := 0; i < b.N; i++ { 21 | SampleVariance(s) 22 | } 23 | } 24 | func BenchmarkCompute1000000(b *testing.B) { 25 | s := make([]int64, 1000000) 26 | for i := 0; i < len(s); i++ { 27 | s[i] = int64(i) 28 | } 29 | b.ResetTimer() 30 | for i := 0; i < b.N; i++ { 31 | SampleVariance(s) 32 | } 33 | } 34 | func BenchmarkCopy1000(b *testing.B) { 35 | s := make([]int64, 1000) 36 | for i := 0; i < len(s); i++ { 37 | s[i] = int64(i) 38 | } 39 | b.ResetTimer() 40 | for i := 0; i < b.N; i++ { 41 | sCopy := make([]int64, len(s)) 42 | copy(sCopy, s) 43 | } 44 | } 45 | func BenchmarkCopy1000000(b *testing.B) { 46 | s := make([]int64, 1000000) 47 | for i := 0; i < len(s); i++ { 48 | s[i] = int64(i) 49 | } 50 | b.ResetTimer() 51 | for i := 0; i < b.N; i++ { 52 | sCopy := make([]int64, len(s)) 53 | copy(sCopy, s) 54 | } 55 | } 56 | 57 | func BenchmarkExpDecaySample257(b *testing.B) { 58 | benchmarkSample(b, NewExpDecaySample(257, 0.015)) 59 | } 60 | 61 | func BenchmarkExpDecaySample514(b *testing.B) { 62 | benchmarkSample(b, NewExpDecaySample(514, 0.015)) 63 | } 64 | 65 | func BenchmarkExpDecaySample1028(b *testing.B) { 66 | benchmarkSample(b, NewExpDecaySample(1028, 0.015)) 67 | } 68 | 69 | func BenchmarkUniformSample257(b *testing.B) { 70 | benchmarkSample(b, NewUniformSample(257)) 71 | } 72 | 73 | func BenchmarkUniformSample514(b *testing.B) { 74 | benchmarkSample(b, NewUniformSample(514)) 75 | } 76 | 77 | func BenchmarkUniformSample1028(b *testing.B) { 78 | benchmarkSample(b, NewUniformSample(1028)) 79 | } 80 | 81 | func TestExpDecaySample10(t *testing.T) { 82 | rand.Seed(1) 83 | s := NewExpDecaySample(100, 0.99) 84 | for i := 0; i < 10; i++ { 85 | s.Update(int64(i)) 86 | } 87 | if size := s.Count(); 10 != size { 88 | t.Errorf("s.Count(): 10 != %v\n", size) 89 | } 90 | if size := s.Size(); 10 != size { 91 | t.Errorf("s.Size(): 10 != %v\n", size) 92 | } 93 | if l := len(s.Values()); 10 != l { 94 | t.Errorf("len(s.Values()): 10 != %v\n", l) 95 | } 96 | for _, v := range s.Values() { 97 | if v > 10 || v < 0 { 98 | t.Errorf("out of range [0, 10): %v\n", v) 99 | } 100 | } 101 | } 102 | 103 | func TestExpDecaySample100(t *testing.T) { 104 | rand.Seed(1) 105 | s := NewExpDecaySample(1000, 0.01) 106 | for i := 0; i < 100; i++ { 107 | s.Update(int64(i)) 108 | } 109 | if size := s.Count(); 100 != size { 110 | t.Errorf("s.Count(): 100 != %v\n", size) 111 | } 112 | if size := s.Size(); 100 != size { 113 | t.Errorf("s.Size(): 100 != %v\n", size) 114 | } 115 | if l := len(s.Values()); 100 != l { 116 | t.Errorf("len(s.Values()): 100 != %v\n", l) 117 | } 118 | for _, v := range s.Values() { 119 | if v > 100 || v < 0 { 120 | t.Errorf("out of range [0, 100): %v\n", v) 121 | } 122 | } 123 | } 124 | 125 | func TestExpDecaySample1000(t *testing.T) { 126 | rand.Seed(1) 127 | s := NewExpDecaySample(100, 0.99) 128 | for i := 0; i < 1000; i++ { 129 | s.Update(int64(i)) 130 | } 131 | if size := s.Count(); 1000 != size { 132 | t.Errorf("s.Count(): 1000 != %v\n", size) 133 | } 134 | if size := s.Size(); 100 != size { 135 | t.Errorf("s.Size(): 100 != %v\n", size) 136 | } 137 | if l := len(s.Values()); 100 != l { 138 | t.Errorf("len(s.Values()): 100 != %v\n", l) 139 | } 140 | for _, v := range s.Values() { 141 | if v > 1000 || v < 0 { 142 | t.Errorf("out of range [0, 1000): %v\n", v) 143 | } 144 | } 145 | } 146 | 147 | // This test makes sure that the sample's priority is not amplified by using 148 | // nanosecond duration since start rather than second duration since start. 149 | // The priority becomes +Inf quickly after starting if this is done, 150 | // effectively freezing the set of samples until a rescale step happens. 151 | func TestExpDecaySampleNanosecondRegression(t *testing.T) { 152 | rand.Seed(1) 153 | s := NewExpDecaySample(100, 0.99) 154 | for i := 0; i < 100; i++ { 155 | s.Update(10) 156 | } 157 | time.Sleep(1 * time.Millisecond) 158 | for i := 0; i < 100; i++ { 159 | s.Update(20) 160 | } 161 | v := s.Values() 162 | avg := float64(0) 163 | for i := 0; i < len(v); i++ { 164 | avg += float64(v[i]) 165 | } 166 | avg /= float64(len(v)) 167 | if avg > 16 || avg < 14 { 168 | t.Errorf("out of range [14, 16]: %v\n", avg) 169 | } 170 | } 171 | 172 | func TestExpDecaySampleSnapshot(t *testing.T) { 173 | now := time.Now() 174 | rand.Seed(1) 175 | s := NewExpDecaySample(100, 0.99) 176 | for i := 1; i <= 10000; i++ { 177 | s.(*ExpDecaySample).update(now.Add(time.Duration(i)), int64(i)) 178 | } 179 | snapshot := s.Snapshot() 180 | s.Update(1) 181 | testExpDecaySampleStatistics(t, snapshot) 182 | } 183 | 184 | func TestExpDecaySampleStatistics(t *testing.T) { 185 | now := time.Now() 186 | rand.Seed(1) 187 | s := NewExpDecaySample(100, 0.99) 188 | for i := 1; i <= 10000; i++ { 189 | s.(*ExpDecaySample).update(now.Add(time.Duration(i)), int64(i)) 190 | } 191 | testExpDecaySampleStatistics(t, s) 192 | } 193 | 194 | func TestUniformSample(t *testing.T) { 195 | rand.Seed(1) 196 | s := NewUniformSample(100) 197 | for i := 0; i < 1000; i++ { 198 | s.Update(int64(i)) 199 | } 200 | if size := s.Count(); 1000 != size { 201 | t.Errorf("s.Count(): 1000 != %v\n", size) 202 | } 203 | if size := s.Size(); 100 != size { 204 | t.Errorf("s.Size(): 100 != %v\n", size) 205 | } 206 | if l := len(s.Values()); 100 != l { 207 | t.Errorf("len(s.Values()): 100 != %v\n", l) 208 | } 209 | for _, v := range s.Values() { 210 | if v > 1000 || v < 0 { 211 | t.Errorf("out of range [0, 100): %v\n", v) 212 | } 213 | } 214 | } 215 | 216 | func TestUniformSampleIncludesTail(t *testing.T) { 217 | rand.Seed(1) 218 | s := NewUniformSample(100) 219 | max := 100 220 | for i := 0; i < max; i++ { 221 | s.Update(int64(i)) 222 | } 223 | v := s.Values() 224 | sum := 0 225 | exp := (max - 1) * max / 2 226 | for i := 0; i < len(v); i++ { 227 | sum += int(v[i]) 228 | } 229 | if exp != sum { 230 | t.Errorf("sum: %v != %v\n", exp, sum) 231 | } 232 | } 233 | 234 | func TestUniformSampleSnapshot(t *testing.T) { 235 | s := NewUniformSample(100) 236 | for i := 1; i <= 10000; i++ { 237 | s.Update(int64(i)) 238 | } 239 | snapshot := s.Snapshot() 240 | s.Update(1) 241 | testUniformSampleStatistics(t, snapshot) 242 | } 243 | 244 | func TestUniformSampleStatistics(t *testing.T) { 245 | rand.Seed(1) 246 | s := NewUniformSample(100) 247 | for i := 1; i <= 10000; i++ { 248 | s.Update(int64(i)) 249 | } 250 | testUniformSampleStatistics(t, s) 251 | } 252 | 253 | func benchmarkSample(b *testing.B, s Sample) { 254 | var memStats runtime.MemStats 255 | runtime.ReadMemStats(&memStats) 256 | pauseTotalNs := memStats.PauseTotalNs 257 | b.ResetTimer() 258 | for i := 0; i < b.N; i++ { 259 | s.Update(1) 260 | } 261 | b.StopTimer() 262 | runtime.GC() 263 | runtime.ReadMemStats(&memStats) 264 | b.Logf("GC cost: %d ns/op", int(memStats.PauseTotalNs-pauseTotalNs)/b.N) 265 | } 266 | 267 | func testExpDecaySampleStatistics(t *testing.T, s Sample) { 268 | if count := s.Count(); 10000 != count { 269 | t.Errorf("s.Count(): 10000 != %v\n", count) 270 | } 271 | if min := s.Min(); 107 != min { 272 | t.Errorf("s.Min(): 107 != %v\n", min) 273 | } 274 | if max := s.Max(); 10000 != max { 275 | t.Errorf("s.Max(): 10000 != %v\n", max) 276 | } 277 | if mean := s.Mean(); 4965.98 != mean { 278 | t.Errorf("s.Mean(): 4965.98 != %v\n", mean) 279 | } 280 | if stdDev := s.StdDev(); 2959.825156930727 != stdDev { 281 | t.Errorf("s.StdDev(): 2959.825156930727 != %v\n", stdDev) 282 | } 283 | ps := s.Percentiles([]float64{0.5, 0.75, 0.99}) 284 | if 4615 != ps[0] { 285 | t.Errorf("median: 4615 != %v\n", ps[0]) 286 | } 287 | if 7672 != ps[1] { 288 | t.Errorf("75th percentile: 7672 != %v\n", ps[1]) 289 | } 290 | if 9998.99 != ps[2] { 291 | t.Errorf("99th percentile: 9998.99 != %v\n", ps[2]) 292 | } 293 | } 294 | 295 | func testUniformSampleStatistics(t *testing.T, s Sample) { 296 | if count := s.Count(); 10000 != count { 297 | t.Errorf("s.Count(): 10000 != %v\n", count) 298 | } 299 | if min := s.Min(); 9412 != min { 300 | t.Errorf("s.Min(): 9412 != %v\n", min) 301 | } 302 | if max := s.Max(); 10000 != max { 303 | t.Errorf("s.Max(): 10000 != %v\n", max) 304 | } 305 | if mean := s.Mean(); 9902.26 != mean { 306 | t.Errorf("s.Mean(): 9902.26 != %v\n", mean) 307 | } 308 | if stdDev := s.StdDev(); 101.8667384380201 != stdDev { 309 | t.Errorf("s.StdDev(): 101.8667384380201 != %v\n", stdDev) 310 | } 311 | ps := s.Percentiles([]float64{0.5, 0.75, 0.99}) 312 | if 9930.5 != ps[0] { 313 | t.Errorf("median: 9930.5 != %v\n", ps[0]) 314 | } 315 | if 9973.75 != ps[1] { 316 | t.Errorf("75th percentile: 9973.75 != %v\n", ps[1]) 317 | } 318 | if 9999.99 != ps[2] { 319 | t.Errorf("99th percentile: 9999.99 != %v\n", ps[2]) 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /executor_test.go: -------------------------------------------------------------------------------- 1 | package goHystrix 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/smartystreets/goconvey/convey" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type ResultCommand struct { 11 | result interface{} 12 | err error 13 | 14 | shouldPanic bool 15 | } 16 | 17 | func (rc *ResultCommand) Run() (interface{}, error) { 18 | if rc.shouldPanic { 19 | panic("I must panic!") 20 | } 21 | return rc.result, rc.err 22 | } 23 | 24 | func CommandOptionsForTest() CommandOptions { 25 | return CommandOptions{ 26 | ErrorsThreshold: 50.0, 27 | MinimumNumberOfRequest: 3, 28 | NumberOfSecondsToStore: 5, 29 | NumberOfSamplesToStore: 10, 30 | Timeout: 3 * time.Millisecond, 31 | } 32 | 33 | } 34 | 35 | func TestRunErrors(t *testing.T) { 36 | Convey("Command returns basic value, no error", t, func() { 37 | CircuitsReset() 38 | command := NewCommandWithOptions("ResultCommand", "testGroup", &ResultCommand{"result", nil, false}, CommandOptionsForTest()) 39 | 40 | Convey("run", func() { 41 | result, err := command.Execute() 42 | 43 | So(err, ShouldBeNil) 44 | So(result, ShouldEqual, "result") 45 | }) 46 | }) 47 | 48 | Convey("Command returns nil, nil", t, func() { 49 | CircuitsReset() 50 | command := NewCommandWithOptions("ResultCommand", "testGroup", &ResultCommand{nil, nil, false}, CommandOptionsForTest()) 51 | 52 | Convey("run", func() { 53 | result, err := command.Execute() 54 | 55 | So(err, ShouldBeNil) 56 | So(result, ShouldBeNil) 57 | }) 58 | }) 59 | 60 | Convey("Command returns value, error", t, func() { 61 | CircuitsReset() 62 | command := NewCommandWithOptions("ResultCommand", "testGroup", &ResultCommand{"result", fmt.Errorf("some error"), false}, CommandOptionsForTest()) 63 | 64 | Convey("run", func() { 65 | result, err := command.Execute() 66 | 67 | So(err, ShouldNotBeNil) 68 | So(result, ShouldBeNil) 69 | }) 70 | }) 71 | 72 | Convey("Command panics!", t, func() { 73 | CircuitsReset() 74 | command := NewCommandWithOptions("ResultCommand", "testGroup", &ResultCommand{nil, nil, true}, CommandOptionsForTest()) 75 | 76 | Convey("run", func() { 77 | result, err := command.Execute() 78 | 79 | So(err, ShouldNotBeNil) 80 | So(result, ShouldBeNil) 81 | So(command.Metric().HealthCounts().Panics, ShouldEqual, 1) 82 | }) 83 | }) 84 | } 85 | 86 | type NoFallbackCommand struct { 87 | state string 88 | } 89 | 90 | func (cmd *NoFallbackCommand) Run() (interface{}, error) { 91 | return "", fmt.Errorf(cmd.state) 92 | } 93 | 94 | func TestRunNoFallback(t *testing.T) { 95 | Convey("Command Execute errors directly, without fallback implementation", t, func() { 96 | CircuitsReset() 97 | errorCommand := NewCommandWithOptions("nofallbackCmd", "testGroup", &NoFallbackCommand{"error"}, CommandOptionsForTest()) 98 | 99 | Convey("After 3 errors, the circuit is open and the next call is using the fallback", func() { 100 | var result interface{} 101 | var err error 102 | 103 | // 1 104 | result, err = errorCommand.Execute() 105 | So(err, ShouldNotBeNil) 106 | So(result, ShouldBeNil) 107 | 108 | // 2 109 | result, err = errorCommand.Execute() 110 | So(err, ShouldNotBeNil) 111 | So(result, ShouldBeNil) 112 | 113 | //3 114 | result, err = errorCommand.Execute() 115 | So(err, ShouldNotBeNil) 116 | So(result, ShouldBeNil) 117 | 118 | // 4 limit reached, falling back 119 | result, err = errorCommand.Execute() 120 | So(err.Error(), ShouldEqual, "[testGroup:nofallbackCmd] FallbackError: No fallback implementation available for nofallbackCmd") 121 | So(result, ShouldBeNil) 122 | So(errorCommand.HealthCounts().Failures, ShouldEqual, 3) 123 | 124 | }) 125 | }) 126 | } 127 | 128 | type StringCommand struct { 129 | state string 130 | fallbackState string 131 | } 132 | 133 | func NewStringCommand(state string, fallbackState string) *Command { 134 | var command *StringCommand 135 | command = &StringCommand{} 136 | command.state = state 137 | command.fallbackState = fallbackState 138 | 139 | return NewCommandWithOptions("testCommand", "testGroup", command, CommandOptionsForTest()) 140 | } 141 | 142 | func (c *StringCommand) Run() (interface{}, error) { 143 | if c.state == "error" { 144 | return nil, fmt.Errorf("ERROR: this method is mend to fail") 145 | } 146 | 147 | if c.state == "timeout" { 148 | time.Sleep(4 * time.Millisecond) 149 | return "time out!", nil 150 | } 151 | 152 | return "hello hystrix world", nil 153 | } 154 | 155 | func (c *StringCommand) Fallback() (interface{}, error) { 156 | if c.fallbackState == "fallbackError" { 157 | return nil, fmt.Errorf("ERROR: error doing fallback") 158 | } 159 | return "FALLBACK", nil 160 | 161 | } 162 | 163 | func TestRunString(t *testing.T) { 164 | 165 | Convey("Command Run returns a string", t, func() { 166 | CircuitsReset() 167 | okCommand := NewStringCommand("ok", "fallbackOk") 168 | 169 | Convey("When Run is executed", func() { 170 | 171 | result, err := okCommand.Run() 172 | 173 | Convey("The result should be the string value", func() { 174 | So(result, ShouldEqual, "hello hystrix world") 175 | }) 176 | 177 | Convey("There is no error", func() { 178 | So(err, ShouldBeNil) 179 | }) 180 | }) 181 | }) 182 | } 183 | 184 | func TestRunError(t *testing.T) { 185 | 186 | Convey("Command Run returns an error", t, func() { 187 | CircuitsReset() 188 | errorCommand := NewStringCommand("error", "fallbackOk") 189 | 190 | Convey("When Run is executed", func() { 191 | result, err := errorCommand.Run() 192 | 193 | Convey("The result should be Nil", func() { 194 | So(result, ShouldBeNil) 195 | }) 196 | 197 | Convey("There is a expected error", func() { 198 | So(err.Error(), ShouldEqual, "ERROR: this method is mend to fail") 199 | }) 200 | }) 201 | }) 202 | } 203 | 204 | func TestExecuteString(t *testing.T) { 205 | 206 | Convey("Command Execute runs properly", t, func() { 207 | CircuitsReset() 208 | okCommand := NewStringCommand("ok", "fallbackOk") 209 | 210 | Convey("When Execute is called", func() { 211 | 212 | result, err := okCommand.Execute() 213 | 214 | Convey("The result should be the string value", func() { 215 | So(result, ShouldEqual, "hello hystrix world") 216 | }) 217 | 218 | Convey("There is no error", func() { 219 | So(err, ShouldBeNil) 220 | }) 221 | }) 222 | }) 223 | } 224 | 225 | func TestFallbackForError(t *testing.T) { 226 | Convey("Command Execute uses the Fallback if an error is returned", t, func() { 227 | CircuitsReset() 228 | errorCommand := NewStringCommand("error", "fallbackOk") 229 | 230 | var result interface{} 231 | var err error 232 | 233 | // 1 234 | result, err = errorCommand.Execute() 235 | So(err, ShouldBeNil) 236 | So(result, ShouldEqual, "FALLBACK") 237 | open, reason := errorCommand.circuit.IsOpen() 238 | So(reason, ShouldEqual, "CLOSE: not enought request") 239 | So(open, ShouldBeFalse) 240 | So(errorCommand.HealthCounts().Failures, ShouldEqual, 1) 241 | So(errorCommand.HealthCounts().Fallback, ShouldEqual, 1) 242 | }) 243 | } 244 | 245 | func TestFallbackForTimeout(t *testing.T) { 246 | Convey("Command Execute uses the Fallback if a timeout is returned", t, func() { 247 | CircuitsReset() 248 | timeoutCommand := NewStringCommand("timeout", "fallbackOk") 249 | 250 | var result interface{} 251 | var err error 252 | 253 | // 1 254 | result, err = timeoutCommand.Execute() 255 | So(err, ShouldBeNil) 256 | So(result, ShouldEqual, "FALLBACK") 257 | open, reason := timeoutCommand.circuit.IsOpen() 258 | So(reason, ShouldEqual, "CLOSE: not enought request") 259 | So(open, ShouldBeFalse) 260 | So(timeoutCommand.HealthCounts().Timeouts, ShouldEqual, 1) 261 | So(timeoutCommand.HealthCounts().Fallback, ShouldEqual, 1) 262 | }) 263 | } 264 | 265 | func TestFallback(t *testing.T) { 266 | 267 | Convey("Command Execute failing for 3 times, opens the circuit", t, func() { 268 | CircuitsReset() 269 | errorCommand := NewStringCommand("error", "fallbackOk") 270 | 271 | // 1 272 | errorCommand.Execute() 273 | open, reason := errorCommand.circuit.IsOpen() 274 | So(reason, ShouldEqual, "CLOSE: not enought request") 275 | So(open, ShouldBeFalse) 276 | 277 | // 2 278 | errorCommand.Execute() 279 | open, reason = errorCommand.circuit.IsOpen() 280 | So(reason, ShouldEqual, "CLOSE: not enought request") 281 | So(open, ShouldBeFalse) 282 | 283 | //3 284 | errorCommand.Execute() 285 | // limit reached 286 | open, reason = errorCommand.circuit.IsOpen() 287 | So(reason, ShouldEqual, "OPEN: to many errors") 288 | So(open, ShouldBeTrue) 289 | So(errorCommand.HealthCounts().Failures, ShouldEqual, 3) 290 | 291 | }) 292 | } 293 | 294 | func TestFallbackError(t *testing.T) { 295 | 296 | Convey("Command Execute the fallback and it returns the fallback error and the nested error", t, func() { 297 | CircuitsReset() 298 | errorCommand := NewStringCommand("error", "fallbackError") 299 | 300 | _, err := errorCommand.Execute() 301 | errorText := "[testGroup:testCommand] FallbackError: ERROR: error doing fallback RunError: ERROR: this method is mend to fail" 302 | So(err.Error(), ShouldEqual, errorText) 303 | 304 | }) 305 | } 306 | 307 | func TestExecuteTimeout(t *testing.T) { 308 | 309 | Convey("Command returns the fallback due to timeout", t, func() { 310 | CircuitsReset() 311 | timeoutCommand := NewStringCommand("timeout", "fallbackOk") 312 | 313 | // 1 314 | timeoutCommand.Execute() 315 | open, reason := timeoutCommand.circuit.IsOpen() 316 | So(reason, ShouldEqual, "CLOSE: not enought request") 317 | So(open, ShouldBeFalse) 318 | 319 | // 2 320 | timeoutCommand.Execute() 321 | open, reason = timeoutCommand.circuit.IsOpen() 322 | So(reason, ShouldEqual, "CLOSE: not enought request") 323 | So(open, ShouldBeFalse) 324 | 325 | //3 326 | timeoutCommand.Execute() 327 | // limit reached 328 | open, reason = timeoutCommand.circuit.IsOpen() 329 | So(reason, ShouldEqual, "OPEN: to many errors") 330 | So(open, ShouldBeTrue) 331 | So(timeoutCommand.HealthCounts().Failures, ShouldEqual, 3) 332 | So(timeoutCommand.HealthCounts().Timeouts, ShouldEqual, 3) 333 | 334 | }) 335 | 336 | } 337 | func TestAsync(t *testing.T) { 338 | Convey("Command run async and returns ok", t, func() { 339 | CircuitsReset() 340 | okCommand := NewStringCommand("ok", "fallbackOk") 341 | 342 | resultChan, errorChan := okCommand.Queue() 343 | var err error 344 | var result interface{} 345 | select { 346 | case result = <-resultChan: 347 | err = nil 348 | case err = <-errorChan: 349 | result = nil 350 | } 351 | 352 | So(result, ShouldEqual, "hello hystrix world") 353 | So(err, ShouldBeNil) 354 | 355 | }) 356 | } 357 | 358 | func TestAsyncFallback(t *testing.T) { 359 | 360 | Convey("Command run async, and if it's failing 3 times, the Circuit will be open", t, func() { 361 | CircuitsReset() 362 | errorCommand := NewStringCommand("error", "fallbackOk") 363 | 364 | var result interface{} 365 | 366 | // 1 fail 367 | resultChan, _ := errorCommand.Queue() 368 | result = <-resultChan 369 | So(result, ShouldEqual, "FALLBACK") 370 | open, reason := errorCommand.circuit.IsOpen() 371 | So(reason, ShouldEqual, "CLOSE: not enought request") 372 | So(open, ShouldBeFalse) 373 | 374 | // 2 fail 375 | resultChan, _ = errorCommand.Queue() 376 | result = <-resultChan 377 | So(result, ShouldEqual, "FALLBACK") 378 | open, reason = errorCommand.circuit.IsOpen() 379 | So(reason, ShouldEqual, "CLOSE: not enought request") 380 | So(open, ShouldBeFalse) 381 | 382 | // 3 fail 383 | resultChan, _ = errorCommand.Queue() 384 | result = <-resultChan 385 | So(result, ShouldEqual, "FALLBACK") 386 | So(errorCommand.HealthCounts().Failures, ShouldEqual, 3) 387 | 388 | // limit reached 389 | open, reason = errorCommand.circuit.IsOpen() 390 | So(reason, ShouldEqual, "OPEN: to many errors") 391 | So(open, ShouldBeTrue) 392 | 393 | // 4 falling back 394 | resultChan, _ = errorCommand.Queue() 395 | result = <-resultChan 396 | So(result, ShouldEqual, "FALLBACK") 397 | So(errorCommand.HealthCounts().Failures, ShouldEqual, 3) 398 | 399 | }) 400 | } 401 | 402 | func TestAsyncFallbackError(t *testing.T) { 403 | 404 | Convey("Command run async, and the fallback returns errors, the Circuit will be open", t, func() { 405 | CircuitsReset() 406 | errorCommand := NewStringCommand("error", "fallbackError") 407 | 408 | var err error 409 | 410 | // 1 fail 411 | _, errorChan := errorCommand.Queue() 412 | err = <-errorChan 413 | 414 | errorText := "[testGroup:testCommand] FallbackError: ERROR: error doing fallback RunError: ERROR: this method is mend to fail" 415 | So(err.Error(), ShouldEqual, errorText) 416 | open, reason := errorCommand.circuit.IsOpen() 417 | So(reason, ShouldEqual, "CLOSE: not enought request") 418 | So(open, ShouldBeFalse) 419 | 420 | // 2 fail 421 | _, errorChan = errorCommand.Queue() 422 | err = <-errorChan 423 | So(err.Error(), ShouldEqual, errorText) 424 | open, reason = errorCommand.circuit.IsOpen() 425 | So(reason, ShouldEqual, "CLOSE: not enought request") 426 | So(open, ShouldBeFalse) 427 | 428 | // 3 fail 429 | _, errorChan = errorCommand.Queue() 430 | err = <-errorChan 431 | So(err.Error(), ShouldEqual, errorText) 432 | open, reason = errorCommand.circuit.IsOpen() 433 | So(errorCommand.HealthCounts().Failures, ShouldEqual, 3) 434 | So(errorCommand.HealthCounts().FallbackErrors, ShouldEqual, 3) 435 | 436 | // limit reached 437 | So(reason, ShouldEqual, "OPEN: to many errors") 438 | So(open, ShouldBeTrue) 439 | }) 440 | } 441 | 442 | func TestAsyncTimeout(t *testing.T) { 443 | Convey("Command run async and if it is timeouting for 3 times the Circuit will be open", t, func() { 444 | var result interface{} 445 | 446 | CircuitsReset() 447 | timeoutCommand := NewStringCommand("timeout", "fallbackOk") 448 | 449 | // 1 timeout 450 | resultChan, _ := timeoutCommand.Queue() 451 | result = <-resultChan 452 | So(result, ShouldEqual, "FALLBACK") 453 | open, reason := timeoutCommand.circuit.IsOpen() 454 | So(reason, ShouldEqual, "CLOSE: not enought request") 455 | So(open, ShouldBeFalse) 456 | 457 | // 2 timeout 458 | resultChan, _ = timeoutCommand.Queue() 459 | result = <-resultChan 460 | 461 | So(result, ShouldEqual, "FALLBACK") 462 | open, reason = timeoutCommand.circuit.IsOpen() 463 | So(reason, ShouldEqual, "CLOSE: not enought request") 464 | So(open, ShouldBeFalse) 465 | 466 | // 3 timeout 467 | resultChan, _ = timeoutCommand.Queue() 468 | result = <-resultChan 469 | So(result, ShouldEqual, "FALLBACK") 470 | open, reason = timeoutCommand.circuit.IsOpen() 471 | So(reason, ShouldEqual, "OPEN: to many errors") 472 | So(open, ShouldBeTrue) 473 | 474 | // 4 falling back 475 | resultChan, _ = timeoutCommand.Queue() 476 | result = <-resultChan 477 | 478 | So(result, ShouldEqual, "FALLBACK") 479 | So(timeoutCommand.HealthCounts().Failures, ShouldEqual, 3) 480 | So(timeoutCommand.HealthCounts().Timeouts, ShouldEqual, 3) 481 | }) 482 | 483 | } 484 | 485 | func TestMetrics(t *testing.T) { 486 | Convey("Command keep the metrics", t, func() { 487 | CircuitsReset() 488 | x := NewStringCommand("ok", "fallbackok") 489 | y := NewStringCommand("error", "fallbackok") 490 | 491 | Convey("When Execute is called multiple times the counters are updated", func() { 492 | x.Execute() // success 493 | x.Execute() // success 494 | y.Execute() // error 495 | y.Execute() // error 496 | y.Execute() // fallback 497 | 498 | Convey("The success and failures counters are correct", func() { 499 | So(x.HealthCounts().Success, ShouldEqual, 2) 500 | So(y.HealthCounts().Success, ShouldEqual, 2) 501 | So(x.HealthCounts().Failures, ShouldEqual, 2) 502 | So(y.HealthCounts().Failures, ShouldEqual, 2) 503 | So(x.HealthCounts().Fallback, ShouldEqual, 3) 504 | So(y.HealthCounts().Fallback, ShouldEqual, 3) 505 | 506 | fmt.Println(x.circuit.ToJSON()) 507 | 508 | }) 509 | 510 | }) 511 | 512 | }) 513 | } 514 | -------------------------------------------------------------------------------- /sample/sample.go: -------------------------------------------------------------------------------- 1 | package sample 2 | 3 | import ( 4 | "container/heap" 5 | "math" 6 | "math/rand" 7 | "sort" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | ) 12 | 13 | const rescaleThreshold = time.Hour 14 | 15 | // Samples maintain a statistically-significant selection of values from 16 | // a stream. 17 | type Sample interface { 18 | Clear() 19 | Count() int64 20 | Max() int64 21 | Mean() float64 22 | Min() int64 23 | Percentile(float64) float64 24 | Percentiles([]float64) []float64 25 | Size() int 26 | Snapshot() Sample 27 | StdDev() float64 28 | Sum() int64 29 | Update(int64) 30 | Values() []int64 31 | Variance() float64 32 | } 33 | 34 | // ExpDecaySample is an exponentially-decaying sample using a forward-decaying 35 | // priority reservoir. See Cormode et al's "Forward Decay: A Practical Time 36 | // Decay Model for Streaming Systems". 37 | // 38 | // 39 | type ExpDecaySample struct { 40 | alpha float64 41 | count int64 42 | mutex sync.Mutex 43 | reservoirSize int 44 | t0, t1 time.Time 45 | values expDecaySampleHeap 46 | } 47 | 48 | // NewExpDecaySample constructs a new exponentially-decaying sample with the 49 | // given reservoir size and alpha. 50 | func NewExpDecaySample(reservoirSize int, alpha float64) Sample { 51 | s := &ExpDecaySample{ 52 | alpha: alpha, 53 | reservoirSize: reservoirSize, 54 | t0: time.Now(), 55 | values: make(expDecaySampleHeap, 0, reservoirSize), 56 | } 57 | s.t1 = time.Now().Add(rescaleThreshold) 58 | return s 59 | } 60 | 61 | // Clear clears all samples. 62 | func (s *ExpDecaySample) Clear() { 63 | s.mutex.Lock() 64 | defer s.mutex.Unlock() 65 | s.count = 0 66 | s.t0 = time.Now() 67 | s.t1 = s.t0.Add(rescaleThreshold) 68 | s.values = make(expDecaySampleHeap, 0, s.reservoirSize) 69 | } 70 | 71 | // Count returns the number of samples recorded, which may exceed the 72 | // reservoir size. 73 | func (s *ExpDecaySample) Count() int64 { 74 | return atomic.LoadInt64(&s.count) 75 | } 76 | 77 | // Max returns the maximum value in the sample, which may not be the maximum 78 | // value ever to be part of the sample. 79 | func (s *ExpDecaySample) Max() int64 { 80 | return SampleMax(s.Values()) 81 | } 82 | 83 | // Mean returns the mean of the values in the sample. 84 | func (s *ExpDecaySample) Mean() float64 { 85 | return SampleMean(s.Values()) 86 | } 87 | 88 | // Min returns the minimum value in the sample, which may not be the minimum 89 | // value ever to be part of the sample. 90 | func (s *ExpDecaySample) Min() int64 { 91 | return SampleMin(s.Values()) 92 | } 93 | 94 | // Percentile returns an arbitrary percentile of values in the sample. 95 | func (s *ExpDecaySample) Percentile(p float64) float64 { 96 | return SamplePercentile(s.Values(), p) 97 | } 98 | 99 | // Percentiles returns a slice of arbitrary percentiles of values in the 100 | // sample. 101 | func (s *ExpDecaySample) Percentiles(ps []float64) []float64 { 102 | return SamplePercentiles(s.Values(), ps) 103 | } 104 | 105 | // Size returns the size of the sample, which is at most the reservoir size. 106 | func (s *ExpDecaySample) Size() int { 107 | s.mutex.Lock() 108 | defer s.mutex.Unlock() 109 | return len(s.values) 110 | } 111 | 112 | // Snapshot returns a read-only copy of the sample. 113 | func (s *ExpDecaySample) Snapshot() Sample { 114 | s.mutex.Lock() 115 | defer s.mutex.Unlock() 116 | values := make([]int64, len(s.values)) 117 | for i, v := range s.values { 118 | values[i] = v.v 119 | } 120 | return &SampleSnapshot{ 121 | count: s.count, 122 | values: values, 123 | } 124 | } 125 | 126 | // StdDev returns the standard deviation of the values in the sample. 127 | func (s *ExpDecaySample) StdDev() float64 { 128 | return SampleStdDev(s.Values()) 129 | } 130 | 131 | // Sum returns the sum of the values in the sample. 132 | func (s *ExpDecaySample) Sum() int64 { 133 | return SampleSum(s.Values()) 134 | } 135 | 136 | // Update samples a new value. 137 | func (s *ExpDecaySample) Update(v int64) { 138 | s.update(time.Now(), v) 139 | } 140 | 141 | // Values returns a copy of the values in the sample. 142 | func (s *ExpDecaySample) Values() []int64 { 143 | s.mutex.Lock() 144 | defer s.mutex.Unlock() 145 | values := make([]int64, len(s.values)) 146 | for i, v := range s.values { 147 | values[i] = v.v 148 | } 149 | return values 150 | } 151 | 152 | // Variance returns the variance of the values in the sample. 153 | func (s *ExpDecaySample) Variance() float64 { 154 | return SampleVariance(s.Values()) 155 | } 156 | 157 | // update samples a new value at a particular timestamp. This is a method all 158 | // its own to facilitate testing. 159 | func (s *ExpDecaySample) update(t time.Time, v int64) { 160 | s.mutex.Lock() 161 | defer s.mutex.Unlock() 162 | s.count++ 163 | if len(s.values) == s.reservoirSize { 164 | heap.Pop(&s.values) 165 | } 166 | heap.Push(&s.values, expDecaySample{ 167 | k: math.Exp(t.Sub(s.t0).Seconds()*s.alpha) / rand.Float64(), 168 | v: v, 169 | }) 170 | if t.After(s.t1) { 171 | values := s.values 172 | t0 := s.t0 173 | s.values = make(expDecaySampleHeap, 0, s.reservoirSize) 174 | s.t0 = t 175 | s.t1 = s.t0.Add(rescaleThreshold) 176 | for _, v := range values { 177 | v.k = v.k * math.Exp(-s.alpha*float64(s.t0.Sub(t0))) 178 | heap.Push(&s.values, v) 179 | } 180 | } 181 | } 182 | 183 | // NilSample is a no-op Sample. 184 | type NilSample struct{} 185 | 186 | // Clear is a no-op. 187 | func (NilSample) Clear() {} 188 | 189 | // Count is a no-op. 190 | func (NilSample) Count() int64 { return 0 } 191 | 192 | // Max is a no-op. 193 | func (NilSample) Max() int64 { return 0 } 194 | 195 | // Mean is a no-op. 196 | func (NilSample) Mean() float64 { return 0.0 } 197 | 198 | // Min is a no-op. 199 | func (NilSample) Min() int64 { return 0 } 200 | 201 | // Percentile is a no-op. 202 | func (NilSample) Percentile(p float64) float64 { return 0.0 } 203 | 204 | // Percentiles is a no-op. 205 | func (NilSample) Percentiles(ps []float64) []float64 { 206 | return make([]float64, len(ps)) 207 | } 208 | 209 | // Size is a no-op. 210 | func (NilSample) Size() int { return 0 } 211 | 212 | // Sample is a no-op. 213 | func (NilSample) Snapshot() Sample { return NilSample{} } 214 | 215 | // StdDev is a no-op. 216 | func (NilSample) StdDev() float64 { return 0.0 } 217 | 218 | // Sum is a no-op. 219 | func (NilSample) Sum() int64 { return 0 } 220 | 221 | // Update is a no-op. 222 | func (NilSample) Update(v int64) {} 223 | 224 | // Values is a no-op. 225 | func (NilSample) Values() []int64 { return []int64{} } 226 | 227 | // Variance is a no-op. 228 | func (NilSample) Variance() float64 { return 0.0 } 229 | 230 | // SampleMax returns the maximum value of the slice of int64. 231 | func SampleMax(values []int64) int64 { 232 | if 0 == len(values) { 233 | return 0 234 | } 235 | var max int64 = math.MinInt64 236 | for _, v := range values { 237 | if max < v { 238 | max = v 239 | } 240 | } 241 | return max 242 | } 243 | 244 | // SampleMean returns the mean value of the slice of int64. 245 | func SampleMean(values []int64) float64 { 246 | if 0 == len(values) { 247 | return 0.0 248 | } 249 | return float64(SampleSum(values)) / float64(len(values)) 250 | } 251 | 252 | // SampleMin returns the minimum value of the slice of int64. 253 | func SampleMin(values []int64) int64 { 254 | if 0 == len(values) { 255 | return 0 256 | } 257 | var min int64 = math.MaxInt64 258 | for _, v := range values { 259 | if min > v { 260 | min = v 261 | } 262 | } 263 | return min 264 | } 265 | 266 | // SamplePercentiles returns an arbitrary percentile of the slice of int64. 267 | func SamplePercentile(values int64Slice, p float64) float64 { 268 | return SamplePercentiles(values, []float64{p})[0] 269 | } 270 | 271 | // SamplePercentiles returns a slice of arbitrary percentiles of the slice of 272 | // int64. 273 | func SamplePercentiles(values int64Slice, ps []float64) []float64 { 274 | scores := make([]float64, len(ps)) 275 | size := len(values) 276 | if size > 0 { 277 | sort.Sort(values) 278 | for i, p := range ps { 279 | pos := p * float64(size+1) 280 | if pos < 1.0 { 281 | scores[i] = float64(values[0]) 282 | } else if pos >= float64(size) { 283 | scores[i] = float64(values[size-1]) 284 | } else { 285 | lower := float64(values[int(pos)-1]) 286 | upper := float64(values[int(pos)]) 287 | scores[i] = lower + (pos-math.Floor(pos))*(upper-lower) 288 | } 289 | } 290 | } 291 | return scores 292 | } 293 | 294 | // SampleSnapshot is a read-only copy of another Sample. 295 | type SampleSnapshot struct { 296 | count int64 297 | values []int64 298 | } 299 | 300 | // Clear panics. 301 | func (*SampleSnapshot) Clear() { 302 | panic("Clear called on a SampleSnapshot") 303 | } 304 | 305 | // Count returns the count of inputs at the time the snapshot was taken. 306 | func (s *SampleSnapshot) Count() int64 { return s.count } 307 | 308 | // Max returns the maximal value at the time the snapshot was taken. 309 | func (s *SampleSnapshot) Max() int64 { return SampleMax(s.values) } 310 | 311 | // Mean returns the mean value at the time the snapshot was taken. 312 | func (s *SampleSnapshot) Mean() float64 { return SampleMean(s.values) } 313 | 314 | // Min returns the minimal value at the time the snapshot was taken. 315 | func (s *SampleSnapshot) Min() int64 { return SampleMin(s.values) } 316 | 317 | // Percentile returns an arbitrary percentile of values at the time the 318 | // snapshot was taken. 319 | func (s *SampleSnapshot) Percentile(p float64) float64 { 320 | return SamplePercentile(s.values, p) 321 | } 322 | 323 | // Percentiles returns a slice of arbitrary percentiles of values at the time 324 | // the snapshot was taken. 325 | func (s *SampleSnapshot) Percentiles(ps []float64) []float64 { 326 | return SamplePercentiles(s.values, ps) 327 | } 328 | 329 | // Size returns the size of the sample at the time the snapshot was taken. 330 | func (s *SampleSnapshot) Size() int { return len(s.values) } 331 | 332 | // Snapshot returns the snapshot. 333 | func (s *SampleSnapshot) Snapshot() Sample { return s } 334 | 335 | // StdDev returns the standard deviation of values at the time the snapshot was 336 | // taken. 337 | func (s *SampleSnapshot) StdDev() float64 { return SampleStdDev(s.values) } 338 | 339 | // Sum returns the sum of values at the time the snapshot was taken. 340 | func (s *SampleSnapshot) Sum() int64 { return SampleSum(s.values) } 341 | 342 | // Update panics. 343 | func (*SampleSnapshot) Update(int64) { 344 | panic("Update called on a SampleSnapshot") 345 | } 346 | 347 | // Values returns a copy of the values in the sample. 348 | func (s *SampleSnapshot) Values() []int64 { 349 | values := make([]int64, len(s.values)) 350 | copy(values, s.values) 351 | return values 352 | } 353 | 354 | // Variance returns the variance of values at the time the snapshot was taken. 355 | func (s *SampleSnapshot) Variance() float64 { return SampleVariance(s.values) } 356 | 357 | // SampleStdDev returns the standard deviation of the slice of int64. 358 | func SampleStdDev(values []int64) float64 { 359 | return math.Sqrt(SampleVariance(values)) 360 | } 361 | 362 | // SampleSum returns the sum of the slice of int64. 363 | func SampleSum(values []int64) int64 { 364 | var sum int64 365 | for _, v := range values { 366 | sum += v 367 | } 368 | return sum 369 | } 370 | 371 | // SampleVariance returns the variance of the slice of int64. 372 | func SampleVariance(values []int64) float64 { 373 | if 0 == len(values) { 374 | return 0.0 375 | } 376 | m := SampleMean(values) 377 | var sum float64 378 | for _, v := range values { 379 | d := float64(v) - m 380 | sum += d * d 381 | } 382 | return sum / float64(len(values)) 383 | } 384 | 385 | // A uniform sample using Vitter's Algorithm R. 386 | // 387 | // 388 | type UniformSample struct { 389 | count int64 390 | mutex sync.Mutex 391 | reservoirSize int 392 | values []int64 393 | } 394 | 395 | // NewUniformSample constructs a new uniform sample with the given reservoir 396 | // size. 397 | func NewUniformSample(reservoirSize int) Sample { 398 | return &UniformSample{reservoirSize: reservoirSize} 399 | } 400 | 401 | // Clear clears all samples. 402 | func (s *UniformSample) Clear() { 403 | s.mutex.Lock() 404 | defer s.mutex.Unlock() 405 | s.count = 0 406 | s.values = make([]int64, 0, s.reservoirSize) 407 | } 408 | 409 | // Count returns the number of samples recorded, which may exceed the 410 | // reservoir size. 411 | func (s *UniformSample) Count() int64 { 412 | return atomic.LoadInt64(&s.count) 413 | } 414 | 415 | // Max returns the maximum value in the sample, which may not be the maximum 416 | // value ever to be part of the sample. 417 | func (s *UniformSample) Max() int64 { 418 | s.mutex.Lock() 419 | defer s.mutex.Unlock() 420 | return SampleMax(s.values) 421 | } 422 | 423 | // Mean returns the mean of the values in the sample. 424 | func (s *UniformSample) Mean() float64 { 425 | s.mutex.Lock() 426 | defer s.mutex.Unlock() 427 | return SampleMean(s.values) 428 | } 429 | 430 | // Min returns the minimum value in the sample, which may not be the minimum 431 | // value ever to be part of the sample. 432 | func (s *UniformSample) Min() int64 { 433 | s.mutex.Lock() 434 | defer s.mutex.Unlock() 435 | return SampleMin(s.values) 436 | } 437 | 438 | // Percentile returns an arbitrary percentile of values in the sample. 439 | func (s *UniformSample) Percentile(p float64) float64 { 440 | s.mutex.Lock() 441 | defer s.mutex.Unlock() 442 | return SamplePercentile(s.values, p) 443 | } 444 | 445 | // Percentiles returns a slice of arbitrary percentiles of values in the 446 | // sample. 447 | func (s *UniformSample) Percentiles(ps []float64) []float64 { 448 | s.mutex.Lock() 449 | defer s.mutex.Unlock() 450 | return SamplePercentiles(s.values, ps) 451 | } 452 | 453 | // Size returns the size of the sample, which is at most the reservoir size. 454 | func (s *UniformSample) Size() int { 455 | s.mutex.Lock() 456 | defer s.mutex.Unlock() 457 | return len(s.values) 458 | } 459 | 460 | // Snapshot returns a read-only copy of the sample. 461 | func (s *UniformSample) Snapshot() Sample { 462 | s.mutex.Lock() 463 | defer s.mutex.Unlock() 464 | values := make([]int64, len(s.values)) 465 | copy(values, s.values) 466 | return &SampleSnapshot{ 467 | count: s.count, 468 | values: values, 469 | } 470 | } 471 | 472 | // StdDev returns the standard deviation of the values in the sample. 473 | func (s *UniformSample) StdDev() float64 { 474 | s.mutex.Lock() 475 | defer s.mutex.Unlock() 476 | return SampleStdDev(s.values) 477 | } 478 | 479 | // Sum returns the sum of the values in the sample. 480 | func (s *UniformSample) Sum() int64 { 481 | s.mutex.Lock() 482 | defer s.mutex.Unlock() 483 | return SampleSum(s.values) 484 | } 485 | 486 | // Update samples a new value. 487 | func (s *UniformSample) Update(v int64) { 488 | s.mutex.Lock() 489 | defer s.mutex.Unlock() 490 | s.count++ 491 | if len(s.values) < s.reservoirSize { 492 | s.values = append(s.values, v) 493 | } else { 494 | s.values[rand.Intn(s.reservoirSize)] = v 495 | } 496 | } 497 | 498 | // Values returns a copy of the values in the sample. 499 | func (s *UniformSample) Values() []int64 { 500 | s.mutex.Lock() 501 | defer s.mutex.Unlock() 502 | values := make([]int64, len(s.values)) 503 | copy(values, s.values) 504 | return values 505 | } 506 | 507 | // Variance returns the variance of the values in the sample. 508 | func (s *UniformSample) Variance() float64 { 509 | s.mutex.Lock() 510 | defer s.mutex.Unlock() 511 | return SampleVariance(s.values) 512 | } 513 | 514 | // expDecaySample represents an individual sample in a heap. 515 | type expDecaySample struct { 516 | k float64 517 | v int64 518 | } 519 | 520 | // expDecaySampleHeap is a min-heap of expDecaySamples. 521 | type expDecaySampleHeap []expDecaySample 522 | 523 | func (q expDecaySampleHeap) Len() int { 524 | return len(q) 525 | } 526 | 527 | func (q expDecaySampleHeap) Less(i, j int) bool { 528 | return q[i].k < q[j].k 529 | } 530 | 531 | func (q *expDecaySampleHeap) Pop() interface{} { 532 | q_ := *q 533 | n := len(q_) 534 | i := q_[n-1] 535 | q_ = q_[0 : n-1] 536 | *q = q_ 537 | return i 538 | } 539 | 540 | func (q *expDecaySampleHeap) Push(x interface{}) { 541 | q_ := *q 542 | n := len(q_) 543 | q_ = q_[0 : n+1] 544 | q_[n] = x.(expDecaySample) 545 | *q = q_ 546 | } 547 | 548 | func (q expDecaySampleHeap) Swap(i, j int) { 549 | q[i], q[j] = q[j], q[i] 550 | } 551 | 552 | type int64Slice []int64 553 | 554 | func (p int64Slice) Len() int { return len(p) } 555 | func (p int64Slice) Less(i, j int) bool { return p[i] < p[j] } 556 | func (p int64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 557 | --------------------------------------------------------------------------------