├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── doc └── img │ ├── barchart.png │ └── drrschema.png ├── drr.go ├── drr_test.go ├── go.mod └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.13.x 4 | before_install: 5 | - go get github.com/mattn/goveralls 6 | script: 7 | - $GOPATH/bin/goveralls -service=travis-ci -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Giulio Micheloni 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deficit Round Robin channels scheduler 2 | 3 | [![GoDoc][GoDoc-Image]][GoDoc-Url] 4 | [![License][License-Image]][License-Url] 5 | [![FOSSA Status][FOSSA-Image]][FOSSA-Url] 6 | [![Build Status][Build-Image]][Build-Url] 7 | [![Coverage Status][Coverage-Image]][Coverage-Url] 8 | [![Go Report Card][Report-Url]][Report-Image] 9 | 10 | [GoDoc-Url]: https://godoc.org/github.com/bigmikes/drr 11 | [GoDoc-Image]: https://godoc.org/github.com/bigmikes/drr?status.svg 12 | [License-Url]: https://opensource.org/licenses/MIT 13 | [License-Image]: https://img.shields.io/badge/License-MIT-yellow.svg 14 | [FOSSA-Url]: https://app.fossa.io/projects/git%2Bgithub.com%2Fbigmikes%2Fdrr?ref=badge_shield 15 | [FOSSA-Image]: https://app.fossa.io/api/projects/git%2Bgithub.com%2Fbigmikes%2Fdrr.svg?type=shield 16 | [Build-Url]: https://travis-ci.org/bigmikes/drr 17 | [Build-Image]: https://travis-ci.org/bigmikes/drr.svg?branch=master 18 | [Coverage-Url]: https://coveralls.io/github/bigmikes/drr 19 | [Coverage-Image]: https://coveralls.io/repos/github/bigmikes/drr/badge.svg 20 | [Report-Url]: https://goreportcard.com/badge/github.com/bigmikes/drr 21 | [Report-Image]: https://goreportcard.com/report/github.com/bigmikes/drr 22 | 23 | ## Introduction 24 | Sometimes, certain messages are more important than others. The drr package provides a generic implementation of [Deficit Round Robin scheduler](https://en.wikipedia.org/wiki/Deficit_round_robin) for Go channels. Through this package, developer can merge multiple input channels into a single output one by enforcing different input rates. 25 | 26 | ## Quick overview on DRR theory 27 | Let's assume you have one single worker goroutine that must handle all the incoming requests. Let's also assume that there are two sources of those requests implemented through a couple of channels. Channel _In_1_ carries the requests with higher priority _P1_, while channel _In_2_ carries the requests with lower priority _P2_. 28 | 29 |

30 | 31 |

32 | 33 | What we observe from channel _Out_ is that input flows _In_1_ and _In_2_ share the output's capacity according to their priorities. That is, flow _In_1_ takes _P1/(P1+P2)_ fraction of output capacity. While flow _In_2_ uses the remaining fraction, _P2/(P1+P2)_. 34 | 35 |

36 | 37 |

38 | 39 | DRR scheduling algorithm does not take into account empty flows (i.e. those that do not have anything to transmit). Therefore, the output capacity is shared among all the non-empty input flows. 40 | 41 | ## API Documentation 42 | Documentation can be found [here](https://pkg.go.dev/github.com/bigmikes/drr?tab=doc). 43 | 44 | ## Example 45 | ```Go 46 | import ( 47 | "context" 48 | "fmt" 49 | 50 | "github.com/bigmikes/drr" 51 | ) 52 | 53 | func sourceRequests(s string) <-chan string { 54 | inChan := make(chan string, 5) 55 | go func() { 56 | defer close(inChan) 57 | for i := 0; i < 5; i++ { 58 | inChan <- s 59 | } 60 | }() 61 | return inChan 62 | } 63 | 64 | func main() { 65 | // Set output channel and create DRR scheduler. 66 | outputChan := make(chan string, 5) 67 | drr, err := drr.NewDRR(outputChan) 68 | if err != nil { 69 | panic(err) 70 | } 71 | 72 | // Register two input channels with priority 3 and 2 respectively. 73 | sourceChan1 := sourceRequests("req1") 74 | drr.Input(3, sourceChan1) 75 | sourceChan2 := sourceRequests("req2") 76 | drr.Input(2, sourceChan2) 77 | 78 | // Start DRR 79 | drr.Start(context.Background()) 80 | 81 | // Consume values from output channels. 82 | // Expected rates are 3/5 for channel with priority 3 83 | // and 2/5 for channel with priority 2. 84 | for out := range outputChan { 85 | fmt.Println(out) 86 | } 87 | } 88 | 89 | // Output: 90 | // req1 91 | // req1 92 | // req1 93 | // req2 94 | // req2 95 | // req1 96 | // req1 97 | // req2 98 | // req2 99 | // req2 100 | ``` 101 | 102 | ## License 103 | The drr package is licensed under the MIT License. Please see the LICENSE file for details. 104 | 105 | ## Contributing and bug reports 106 | This package surely needs your help and feedbacks. You are welcome to open a new issue [here on GitHub](https://github.com/bigmikes/drr/issues). 107 | -------------------------------------------------------------------------------- /doc/img/barchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmichelo/drr/71928754252649558474ebc7ce79da66f86f4a30/doc/img/barchart.png -------------------------------------------------------------------------------- /doc/img/drrschema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmichelo/drr/71928754252649558474ebc7ce79da66f86f4a30/doc/img/drrschema.png -------------------------------------------------------------------------------- /drr.go: -------------------------------------------------------------------------------- 1 | // Package drr provides a simple generic implementation of Deficit Round Robin 2 | // scheduler for channels. 3 | package drr 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "reflect" 9 | ) 10 | 11 | var ( 12 | // ErrInvalidPriorityValue error is returned by Input method when 13 | // priority value is less than or equal to 0. 14 | ErrInvalidPriorityValue = errors.New("ErrInvalidPriorityValue") 15 | // ErrChannelIsNil error is returned by NewDRR and Input methods 16 | // when channel is nil. 17 | ErrChannelIsNil = errors.New("ErrChannelIsNil") 18 | // ErrContextIsNil is returned by Start method when context.Context 19 | // is nil 20 | ErrContextIsNil = errors.New("ContextIsNil") 21 | ) 22 | 23 | type flow[T any] struct { 24 | c <-chan T 25 | prio int 26 | } 27 | 28 | // DRR is a Deficit Round Robin scheduler, as detailed in 29 | // https://en.wikipedia.org/wiki/Deficit_round_robin. 30 | type DRR[T any] struct { 31 | flows []flow[T] 32 | outChan chan T 33 | flowsToDelete []int 34 | } 35 | 36 | // NewDRR creates a new DRR with indicated output channel. 37 | // 38 | // The outChan must be non-nil, otherwise NewDRR returns 39 | // ErrChannelIsNil error. 40 | func NewDRR[T any](outChan chan T) (*DRR[T], error) { 41 | if outChan == nil { 42 | return nil, ErrChannelIsNil 43 | } 44 | return &DRR[T]{ 45 | outChan: outChan, 46 | }, nil 47 | } 48 | 49 | // Input registers a new ingress flow, that is a channel with 50 | // priority. 51 | // 52 | // Input returns ErrChannelIsNil if input channel is nil. 53 | // Priority must be greater than 0, otherwise Input returns 54 | // ErrInvalidPriorityValue error. 55 | func (d *DRR[T]) Input(prio int, in <-chan T) error { 56 | if prio <= 0 { 57 | return ErrInvalidPriorityValue 58 | } 59 | if in == nil { 60 | return ErrChannelIsNil 61 | } 62 | d.flows = append(d.flows, flow[T]{c: in, prio: prio}) 63 | return nil 64 | } 65 | 66 | // Start actually spawns the DRR goroutine. Once Start is called, 67 | // the goroutine starts forwarding from input channels previously registered 68 | // through Input method to output channel. 69 | // 70 | // Start returns ContextIsNil error if ctx is nil. 71 | // 72 | // DRR goroutine exits when context.Context expires or when all the input 73 | // channels are closed. DRR goroutine closes the output channel upon termination. 74 | func (d *DRR[T]) Start(ctx context.Context) error { 75 | if ctx == nil { 76 | return ErrContextIsNil 77 | } 78 | go func() { 79 | defer close(d.outChan) 80 | for { 81 | // Wait for at least one channel to be ready 82 | readyIndex, value, ok := d.getReadyChannel( 83 | ctx, 84 | d.flows) 85 | if readyIndex < 0 { 86 | // Context expired, exit 87 | return 88 | } 89 | flowLoop: 90 | for index, flow := range d.flows { 91 | dc := flow.prio 92 | if readyIndex == index { 93 | if !ok { 94 | // Chan got closed, remove it from internal slice 95 | d.prepareToUnregister(index) 96 | continue flowLoop 97 | } else { 98 | // This chan triggered the reflect.Select statement 99 | // transmit its value and decrement its deficit counter 100 | d.outChan <- value 101 | dc = flow.prio - 1 102 | } 103 | } 104 | // Trasmit from channel until it has nothing else to send 105 | // or its DC reaches 0 106 | for i := 0; i < dc; i++ { 107 | //First, check if context expired 108 | select { 109 | case <-ctx.Done(): 110 | // Context expired, exit 111 | return 112 | default: 113 | } 114 | //Then, read from input chan 115 | select { 116 | case val, ok := <-flow.c: 117 | if !ok { 118 | // Chan got closed, remove it from internal slice 119 | d.prepareToUnregister(index) 120 | continue flowLoop 121 | } else { 122 | d.outChan <- val 123 | } 124 | default: 125 | continue flowLoop 126 | } 127 | } 128 | } 129 | // All channel closed in this execution can now be actually removed 130 | last := d.unregisterFlows() 131 | if last { 132 | return 133 | } 134 | } 135 | }() 136 | return nil 137 | } 138 | 139 | func (d *DRR[T]) prepareToUnregister(index int) { 140 | d.flowsToDelete = append(d.flowsToDelete, index) 141 | } 142 | 143 | func (d *DRR[T]) unregisterFlows() bool { 144 | oldFlows := d.flows 145 | d.flows = make([]flow[T], 0, len(oldFlows)-len(d.flowsToDelete)) 146 | oldFlowsLoop: 147 | for i, flow := range oldFlows { 148 | for _, index := range d.flowsToDelete { 149 | if index == i { 150 | continue oldFlowsLoop 151 | } 152 | } 153 | d.flows = append(d.flows, flow) 154 | } 155 | d.flowsToDelete = []int{} 156 | return len(d.flows) == 0 157 | } 158 | 159 | func (d *DRR[T]) getReadyChannel(ctx context.Context, flows []flow[T]) (int, T, bool) { 160 | cases := make([]reflect.SelectCase, 0, len(flows)+1) 161 | //First case is the termiantion channel for context cancellation 162 | c := reflect.SelectCase{ 163 | Dir: reflect.SelectRecv, 164 | Chan: reflect.ValueOf(ctx.Done()), 165 | } 166 | cases = append(cases, c) 167 | //Create list of SelectCase 168 | for _, f := range flows { 169 | c := reflect.SelectCase{ 170 | Dir: reflect.SelectRecv, 171 | Chan: reflect.ValueOf(f.c), 172 | } 173 | cases = append(cases, c) 174 | } 175 | //Call Select on all channels 176 | index, value, ok := reflect.Select(cases) 177 | //Termination channel 178 | if index == 0 { 179 | var zeroT T 180 | return -1, zeroT, false 181 | } 182 | //Rescaling index (-1) because of additional termination channel 183 | return index - 1, value.Interface().(T), ok 184 | } 185 | -------------------------------------------------------------------------------- /drr_test.go: -------------------------------------------------------------------------------- 1 | package drr 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "testing" 9 | 10 | . "github.com/smartystreets/goconvey/convey" 11 | ) 12 | 13 | const ( 14 | chanSize = 10 15 | ) 16 | 17 | func generator(prefix string, n int) chan string { 18 | payload := make([]string, n) 19 | for i := 0; i < n; i++ { 20 | payload[i] = fmt.Sprintf("%s: %d", prefix, i) 21 | } 22 | return generatorWithPayload(payload) 23 | } 24 | 25 | func generatorWithPayload(payload []string) chan string { 26 | out := make(chan string, chanSize) 27 | go func() { 28 | for _, msg := range payload { 29 | out <- msg 30 | } 31 | close(out) 32 | }() 33 | return out 34 | } 35 | 36 | func TestNewDRR(t *testing.T) { 37 | Convey("Create new DRR", t, func() { 38 | outChan := make(chan string, 10) 39 | drr, err := NewDRR(outChan) 40 | So(drr, ShouldNotEqual, nil) 41 | So(err, ShouldEqual, nil) 42 | }) 43 | } 44 | 45 | func TestDRR(t *testing.T) { 46 | outChan := make(chan string, 10) 47 | drr, _ := NewDRR(outChan) 48 | 49 | Convey("Register flow", t, func() { 50 | flow1 := generator("flow1", 5) 51 | flow2 := generator("flow2", 5) 52 | drr.Input(2, flow1) 53 | drr.Input(1, flow2) 54 | }) 55 | 56 | Convey("Check output", t, func() { 57 | drr.Start(context.TODO()) 58 | for out := range outChan { 59 | So(out, ShouldNotEqual, "") 60 | } 61 | }) 62 | } 63 | 64 | func TestIntegrityAndOrder(t *testing.T) { 65 | nFlows := 100 66 | flowSize := 100 67 | outChan := make(chan string, 10) 68 | drr, _ := NewDRR(outChan) 69 | 70 | var flows []chan string 71 | payloads := make(map[int][]string) 72 | Convey("Prepare flow with known payload", t, func() { 73 | for flowID := 0; flowID < nFlows; flowID++ { 74 | payload := make([]string, 0, flowSize) 75 | for x := 0; x < flowSize; x++ { 76 | msg := fmt.Sprintf("%d:%d", flowID, x) 77 | payload = append(payload, msg) 78 | } 79 | payloads[flowID] = payload 80 | flows = append(flows, generatorWithPayload(payload)) 81 | } 82 | }) 83 | 84 | Convey("Register all flows", t, func() { 85 | for prio, f := range flows { 86 | drr.Input(prio+1, f) 87 | } 88 | }) 89 | 90 | Convey("Check output w.r.t. known payloads", t, func() { 91 | drr.Start(context.TODO()) 92 | outputPayloads := make(map[int][]string) 93 | for out := range outChan { 94 | So(out, ShouldNotEqual, "") 95 | flowID := getFlowID(out) 96 | outputPayloads[flowID] = append(outputPayloads[flowID], out) 97 | } 98 | 99 | So(len(outputPayloads), ShouldEqual, len(payloads)) 100 | for flowID, payload := range payloads { 101 | outPayload := outputPayloads[flowID] 102 | for i, val := range payload { 103 | out := outPayload[i] 104 | if val != out { 105 | t.Fatalf("for flow %d wanted %v instead of %v", flowID, val, out) 106 | } 107 | } 108 | } 109 | }) 110 | } 111 | 112 | func getFlowID(s string) int { 113 | idStr := strings.Split(s, ":")[0] 114 | id, err := strconv.Atoi(idStr) 115 | if err != nil { 116 | panic(fmt.Errorf("convert of string %s failed: %w", s, err)) 117 | } 118 | return id 119 | } 120 | 121 | func TestMeasureOutputRate(t *testing.T) { 122 | nFlows := 100 123 | flowSize := 10000 124 | outChan := make(chan int, flowSize) 125 | drr, _ := NewDRR(outChan) 126 | var flows []chan int 127 | Convey("Prepare flow with known payload", t, func() { 128 | for flowID := 0; flowID < nFlows; flowID++ { 129 | inChan := make(chan int, flowSize) 130 | for x := 0; x < flowSize; x++ { 131 | inChan <- flowID 132 | } 133 | flows = append(flows, inChan) 134 | } 135 | }) 136 | expectedRates := make(map[int]float64) 137 | totalPrio := float64(0) 138 | Convey("Register all flows", t, func() { 139 | for flowID, f := range flows { 140 | prio := flowID + 1 141 | drr.Input(prio, f) 142 | expectedRates[flowID] = float64(prio) 143 | totalPrio += float64(prio) 144 | } 145 | for flowID := range expectedRates { 146 | expectedRates[flowID] /= totalPrio 147 | } 148 | }) 149 | Convey("Check output w.r.t. known payloads", t, func() { 150 | drr.Start(context.TODO()) 151 | hist := make(map[int]int) 152 | for i := 0; i < flowSize; i++ { 153 | flowID := <-outChan 154 | hist[flowID]++ 155 | } 156 | 157 | for flowID := range hist { 158 | outputRates := float64(hist[flowID]) / float64(flowSize) 159 | So(outputRates, ShouldAlmostEqual, expectedRates[flowID], .01) 160 | } 161 | }) 162 | } 163 | 164 | func TestErrorInput(t *testing.T) { 165 | Convey("Create DRR by passing nil output chan", t, func() { 166 | drr, err := NewDRR[int](nil) 167 | So(drr, ShouldEqual, nil) 168 | So(err, ShouldEqual, ErrChannelIsNil) 169 | }) 170 | Convey("Create DRR and pass wrong values in Input API", t, func() { 171 | drr, _ := NewDRR(make(chan string)) 172 | err := drr.Input(0, make(chan string)) 173 | So(err, ShouldEqual, ErrInvalidPriorityValue) 174 | err = drr.Input(1, nil) 175 | So(err, ShouldEqual, ErrChannelIsNil) 176 | }) 177 | Convey("Create DRR and pass wrong values in Input API", t, func() { 178 | drr, _ := NewDRR(make(chan string)) 179 | err := drr.Start(nil) 180 | So(err, ShouldEqual, ErrContextIsNil) 181 | }) 182 | } 183 | 184 | func TestContextExipre(t *testing.T) { 185 | Convey("Create an empty DRR, start it and cancel the context", t, func() { 186 | outChan := make(chan string) 187 | drr, _ := NewDRR(outChan) 188 | ctx, cancel := context.WithCancel(context.Background()) 189 | err := drr.Start(ctx) 190 | So(err, ShouldEqual, nil) 191 | cancel() 192 | val, ok := <-outChan 193 | So(val, ShouldEqual, "") 194 | So(ok, ShouldEqual, false) 195 | }) 196 | 197 | Convey("Create DRR with one flow, start it and cancel the context", t, func() { 198 | outChan := make(chan string) 199 | drr, _ := NewDRR(outChan) 200 | flow := generator("flow", 5) 201 | drr.Input(10, flow) 202 | ctx, cancel := context.WithCancel(context.Background()) 203 | err := drr.Start(ctx) 204 | So(err, ShouldEqual, nil) 205 | val, ok := <-outChan 206 | So(val, ShouldNotEqual, "") 207 | So(ok, ShouldEqual, true) 208 | cancel() 209 | val, ok = <-outChan 210 | So(val, ShouldNotEqual, "") 211 | So(ok, ShouldEqual, true) 212 | val, ok = <-outChan 213 | So(val, ShouldEqual, "") 214 | So(ok, ShouldEqual, false) 215 | }) 216 | } 217 | 218 | func BenchmarkOverheadUnloaded(b *testing.B) { 219 | outChan := make(chan int) 220 | inChan := make(chan int) 221 | drr, _ := NewDRR(outChan) 222 | drr.Input(10, inChan) 223 | drr.Start(context.TODO()) 224 | b.ResetTimer() 225 | for i := 0; i < b.N; i++ { 226 | inChan <- 5 227 | <-outChan 228 | } 229 | } 230 | 231 | func ExampleDRR() { 232 | chanSize := 5 233 | outChan := make(chan string, chanSize) 234 | // Create new DRR 235 | drr, _ := NewDRR(outChan) 236 | 237 | // First input channel with priority = 3 238 | inChan1 := make(chan string, chanSize) 239 | prio1 := 3 240 | // Prepare known workload 241 | for i := 0; i < chanSize; i++ { 242 | inChan1 <- "chan1" 243 | } 244 | // Register channel into DRR 245 | drr.Input(prio1, inChan1) 246 | 247 | // Second input channel with priority = 2 248 | inChan2 := make(chan string, chanSize) 249 | prio2 := 2 250 | // Prepare known workload 251 | for i := 0; i < chanSize; i++ { 252 | inChan2 <- "chan2" 253 | } 254 | // Register channel into DRR 255 | drr.Input(prio2, inChan2) 256 | 257 | // Start DRR scheduler goroutine 258 | drr.Start(context.Background()) 259 | 260 | // Check the output: over 5 output values 261 | // 3/5 of them should come from first channel 262 | // with priority 3 and 2/5 should come from second 263 | // channel with priority 2. 264 | for i := 0; i < chanSize; i++ { 265 | str := <-outChan 266 | fmt.Println(str) 267 | } 268 | 269 | // Output: 270 | // chan1 271 | // chan1 272 | // chan1 273 | // chan2 274 | // chan2 275 | } 276 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bigmikes/drr 2 | 3 | go 1.19 4 | 5 | require github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 6 | 7 | require ( 8 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect 9 | github.com/jtolds/gls v4.20.0+incompatible // indirect 10 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 2 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 3 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 4 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 5 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 6 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 7 | github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= 8 | github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 9 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 10 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 11 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 12 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 13 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 14 | --------------------------------------------------------------------------------