Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
module go.linecorp.com/garr

require github.com/valyala/fastrand v1.1.0

require (
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.1
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
1 change: 1 addition & 0 deletions http-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# http client
47 changes: 47 additions & 0 deletions http-client/actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2022 LINE Corporation
//
// LINE Corporation licenses this file to you under the Apache License,
// version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at:
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package httpclient

import "net/http"

// EndpointAction specifies in which cases a request should be passed
// to the next endpoint or retrying on current endpoint.
//
// Similar to: http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_next_upstream
type EndpointAction byte

const (
// None indicates that there is no need for taking any actions.
None EndpointAction = iota

// NextEndpoint indicates that client should retry request on next endpoint.
NextEndpoint

// Retrying indicates that client could retry request on same endpoint.
Retrying
)

// OnStatus5xx is builtin decider which judge on 5xx status code.
func OnStatus5xx(statusCode int, _ http.Header) (action EndpointAction) {
switch statusCode {
case http.StatusBadGateway:
action = Retrying
case http.StatusInternalServerError, http.StatusServiceUnavailable:
action = NextEndpoint
default:
action = None
}
return
}
38 changes: 38 additions & 0 deletions http-client/actions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2022 LINE Corporation
//
// LINE Corporation licenses this file to you under the Apache License,
// version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at:
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package httpclient

import (
"net/http"
"testing"
)

func TestOnStatus5xx(t *testing.T) {
if action := OnStatus5xx(http.StatusBadGateway, nil); action != Retrying {
t.FailNow()
}

if action := OnStatus5xx(http.StatusInternalServerError, nil); action != NextEndpoint {
t.FailNow()
}

if action := OnStatus5xx(http.StatusServiceUnavailable, nil); action != NextEndpoint {
t.FailNow()
}

if action := OnStatus5xx(http.StatusOK, nil); action != None {
t.FailNow()
}
}
36 changes: 36 additions & 0 deletions http-client/balancer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2022 LINE Corporation
//
// LINE Corporation licenses this file to you under the Apache License,
// version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at:
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package httpclient

// LB is load balancer for endpoints.
type LB interface {
// Initialize endpoints for LB.
Initialize(endpoints Endpoints)

// Endpoints returned saved/ordered endpoints of LB.
Endpoints() Endpoints

// Pick returned index of picked endpoint.
Pick() (index int)
}

// LBBuilder is builder for LB.
type LBBuilder interface {
Build() LB
}

func defaultLBBuilder() LBBuilder {
return &RoundRobinLBBuilder{}
}
43 changes: 43 additions & 0 deletions http-client/balancer_pf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2022 LINE Corporation
//
// LINE Corporation licenses this file to you under the Apache License,
// version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at:
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package httpclient

// PickfirstLB is pickfirst load balancer.
type PickfirstLB struct {
endpoints Endpoints
}

// Initialize endpoints for PickfirstLB.
func (p *PickfirstLB) Initialize(endpoints Endpoints) {
p.endpoints = endpoints
}

// Endpoints returned saved endpoints inside PickfirstLB
func (p *PickfirstLB) Endpoints() Endpoints {
return p.endpoints
}

// Pick returns index of picked endpoint.
func (p *PickfirstLB) Pick() int {
return 0
}

// PickfirstLBBuilder is builder for Pickfirst load-balancer
type PickfirstLBBuilder struct{}

// Build pickfirst balancer.
func (p *PickfirstLBBuilder) Build() LB {
return &PickfirstLB{}
}
78 changes: 78 additions & 0 deletions http-client/balancer_pf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2022 LINE Corporation
//
// LINE Corporation licenses this file to you under the Apache License,
// version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at:
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package httpclient

import (
"testing"
"time"
)

func TestPickFirst(t *testing.T) {
builder := &PickfirstLBBuilder{}

lb := builder.Build()
lb.Initialize(validEndpoints())

endpoints := lb.Endpoints()
valid := validEndpoints()
if len(endpoints) != len(valid) {
t.FailNow()
}
for i := range endpoints {
if !endpoints[i].Equal(valid[i]) {
t.FailNow()
}
}
if lb.Pick()+lb.Pick()+lb.Pick() != 0 {
t.FailNow()
}
}

func TestPickFirstRace(t *testing.T) {
builder := PickfirstLBBuilder{}

lb := builder.Build()
lb.Initialize(validEndpoints())

type counter struct {
v [3]int
}
ch := make(chan counter, 5)
for i := 0; i < 5; i++ {
go func() {
time.Sleep(200 * time.Millisecond)

var counting counter
for j := 0; j < 18000; j++ {
counting.v[lb.Pick()]++
}

ch <- counting
}()
}

var sum counter
for i := 0; i < 5; i++ {
v := <-ch
sum.v[0] += v.v[0]
sum.v[1] += v.v[1]
sum.v[2] += v.v[2]
}

// 90000 = 18000 * 5
if sum.v[0] != 90000 || sum.v[1] != 0 || sum.v[2] != 0 {
t.FailNow()
}
}
55 changes: 55 additions & 0 deletions http-client/balancer_rr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2022 LINE Corporation
//
// LINE Corporation licenses this file to you under the Apache License,
// version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at:
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package httpclient

import (
"sync/atomic"

"github.com/valyala/fastrand"
)

// RoundRobinLB is round robin strategy.
type RoundRobinLB struct {
endpoints Endpoints
index uint32
n uint32
}

// Initialize endpoints for RoundRobinLB.
func (p *RoundRobinLB) Initialize(endpoints Endpoints) {
p.endpoints = endpoints
p.n = uint32(len(endpoints))
}

// Endpoints returned saved endpoints inside RoundRobinLB
func (p *RoundRobinLB) Endpoints() Endpoints {
return p.endpoints
}

// Pick returns index of picked endpoint.
func (p *RoundRobinLB) Pick() (chosen int) {
if p.n > 0 {
chosen = int(atomic.AddUint32(&p.index, 1) % p.n)
}
return
}

// RoundRobinLBBuilder is builder for RoundRobin load-balancer
type RoundRobinLBBuilder struct{}

// Build round robin balancer.
func (p *RoundRobinLBBuilder) Build() LB {
return &RoundRobinLB{index: fastrand.Uint32()}
}
78 changes: 78 additions & 0 deletions http-client/balancer_rr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2022 LINE Corporation
//
// LINE Corporation licenses this file to you under the Apache License,
// version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at:
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package httpclient

import (
"testing"
"time"
)

func TestRoundRobin(t *testing.T) {
builder := defaultLBBuilder()

lb := builder.Build()
lb.Initialize(validEndpoints())

endpoints := lb.Endpoints()
valid := validEndpoints()
if len(endpoints) != len(valid) {
t.FailNow()
}
for i := range endpoints {
if !endpoints[i].Equal(valid[i]) {
t.FailNow()
}
}
if lb.Pick()+lb.Pick()+lb.Pick() != 3 {
t.FailNow()
}
}

func TestRoundRobinRace(t *testing.T) {
builder := defaultLBBuilder()

lb := builder.Build()
lb.Initialize(validEndpoints())

type counter struct {
v [3]int
}
ch := make(chan counter, 5)
for i := 0; i < 7; i++ {
go func() {
time.Sleep(200 * time.Millisecond)

var counting counter
for j := 0; j < 18000; j++ {
counting.v[lb.Pick()]++
}

ch <- counting
}()
}

var sum counter
for i := 0; i < 7; i++ {
v := <-ch
sum.v[0] += v.v[0]
sum.v[1] += v.v[1]
sum.v[2] += v.v[2]
}

// 42000 = 18000 / 3 * 7
if sum.v[0] != 42000 || sum.v[1] != 42000 || sum.v[2] != 42000 {
t.FailNow()
}
}
Loading