├── .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 |
--------------------------------------------------------------------------------