Skip to content

Commit 78b8a5d

Browse files
authored
Merge pull request #21 from tailingchen/dynamic-discover-ethclient-list
Dynamic discover ethclients in k8s cluster
2 parents e844eef + c99e948 commit 78b8a5d

5 files changed

Lines changed: 525 additions & 188 deletions

File tree

multiclient/client.go

Lines changed: 86 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -34,74 +34,78 @@ import (
3434

3535
const (
3636
dialTimeout = 5 * time.Second
37+
retryPeriod = 10 * time.Second
3738
)
3839

3940
var (
4041
ErrInvalidTypeCast = errors.New("invalid type cast")
4142
ErrNoEthClient = errors.New("no eth client")
42-
43-
retryPeriod = 10 * time.Second
4443
)
4544

4645
type Client struct {
46+
ctx context.Context
47+
cancel context.CancelFunc
4748
rpcClientMap *Map
49+
retrydialWg sync.WaitGroup
4850
}
4951

5052
func New(ctx context.Context, opts ...Option) (*Client, error) {
53+
// create client own context to control the internal go routines
54+
myCtx, myCancel := context.WithCancel(context.Background())
5155
mc := &Client{
56+
ctx: myCtx,
57+
cancel: myCancel,
5258
rpcClientMap: NewMap(),
5359
}
60+
61+
var newErr error
62+
defer func() {
63+
if newErr != nil {
64+
// cancel go routines
65+
mc.Close()
66+
}
67+
}()
68+
5469
for _, opt := range opts {
55-
if err := opt(mc); err != nil {
56-
return nil, err
70+
if newErr = opt(mc); newErr != nil {
71+
return nil, newErr
5772
}
5873
}
5974

60-
urls := mc.rpcClientMap.Keys()
61-
lens := len(urls)
62-
if lens == 0 {
63-
return nil, ErrNoEthClient
64-
}
75+
// Dial each eth client
76+
mc.DialClients(ctx)
6577

66-
log.Debug("Create multiclient", "urls", lens)
67-
errCh := make(chan error, lens)
68-
for _, rawURL := range urls {
69-
go func(ctx context.Context, rawURL string) {
70-
ctx, cancel := context.WithTimeout(ctx, dialTimeout)
71-
defer cancel()
72-
c, err := rpc.DialContext(ctx, rawURL)
73-
if err == nil {
74-
log.Info("Connect to ethclient successfully", "url", rawURL)
75-
mc.rpcClientMap.Set(rawURL, c)
76-
} else {
77-
log.Error("Failed to dial eth client", "rawURL", rawURL, "err", err)
78-
}
79-
errCh <- err
80-
}(ctx, rawURL)
78+
// Return error if have no available eth client.
79+
if len(mc.rpcClientMap.List()) == 0 {
80+
newErr = ErrNoEthClient
81+
return nil, newErr
8182
}
8283

83-
var dialErr error
84-
for i := 0; i < lens; i++ {
85-
err := <-errCh
86-
if err != nil {
87-
dialErr = err
88-
}
89-
}
90-
if dialErr != nil {
91-
mc.Close()
92-
return nil, dialErr
93-
}
84+
mc.retrydialWg.Add(1)
85+
go mc.retrydial()
86+
9487
return mc, nil
9588
}
9689

9790
// Close closes an existing RPC connection.
9891
func (mc *Client) Close() {
92+
// stop go routines
93+
mc.cancel()
94+
mc.retrydialWg.Wait()
9995
clients := mc.rpcClientMap.List()
10096
for _, c := range clients {
10197
c.Close()
10298
}
10399
}
104100

101+
func (mc *Client) Context() context.Context {
102+
return mc.ctx
103+
}
104+
105+
func (mc *Client) ClientMap() *Map {
106+
return mc.rpcClientMap
107+
}
108+
105109
func (mc *Client) EthClients() []*ethclient.Client {
106110
clients := mc.rpcClientMap.List()
107111
ethClients := make([]*ethclient.Client, len(clients))
@@ -727,3 +731,50 @@ func postToAll(ctx context.Context, fns []postFn) error {
727731
}
728732
return lastError
729733
}
734+
735+
type dialedClient struct {
736+
url string
737+
client *rpc.Client
738+
}
739+
740+
func (mc *Client) DialClients(ctx context.Context) {
741+
urls := mc.rpcClientMap.NilClients()
742+
dialCh := make(chan *dialedClient, len(urls))
743+
for _, rawURL := range urls {
744+
go func(rawURL string) {
745+
dialCtx, cancel := context.WithTimeout(ctx, dialTimeout)
746+
defer cancel()
747+
c, err := rpc.DialContext(dialCtx, rawURL)
748+
if err == nil {
749+
log.Info("Connect to eth client successfully", "url", rawURL)
750+
} else {
751+
log.Warn("Failed to dial eth client", "url", rawURL, "err", err)
752+
}
753+
dialCh <- &dialedClient{url: rawURL, client: c}
754+
}(rawURL)
755+
}
756+
757+
for i := 0; i < len(urls); i++ {
758+
dialed := <-dialCh
759+
if dialed.client != nil {
760+
mc.rpcClientMap.Replace(dialed.url, dialed.client)
761+
}
762+
}
763+
}
764+
765+
func (mc *Client) retrydial() {
766+
defer mc.retrydialWg.Done()
767+
768+
ticker := time.NewTicker(retryPeriod)
769+
defer ticker.Stop()
770+
771+
for {
772+
select {
773+
case <-ticker.C:
774+
mc.DialClients(mc.ctx)
775+
case <-mc.ctx.Done():
776+
// mc is closed, stop retry
777+
return
778+
}
779+
}
780+
}

multiclient/consul.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2018 AMIS Technologies
2+
// This file is part of the hypereth library.
3+
//
4+
// The hypereth library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The hypereth library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the hypereth library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package multiclient
18+
19+
import (
20+
"fmt"
21+
netURL "net/url"
22+
23+
"github.com/getamis/sirius/log"
24+
consulAPI "github.com/hashicorp/consul/api"
25+
)
26+
27+
// ConsulDiscovery discovers the dynamic ethclient endpoints through consul server.
28+
// TODO: should watch the change of endpoints
29+
func ConsulDiscovery(rawURL, serviceID, serviceScheme string) Option {
30+
return func(mc *Client) error {
31+
client, err := createConsulClient(rawURL)
32+
if err != nil {
33+
return err
34+
}
35+
urls, err := getEthURLsFromConsul(client, serviceID, serviceScheme)
36+
if err != nil {
37+
return err
38+
}
39+
log.Info("EthClients from consul", "urls", urls)
40+
for _, url := range urls {
41+
mc.ClientMap().Set(url, nil)
42+
}
43+
return nil
44+
}
45+
}
46+
47+
func getEthURLsFromConsul(client *consulAPI.Client, serviceID, serviceScheme string) ([]string, error) {
48+
list, _, err := client.Catalog().Service(serviceID, "", nil)
49+
if err != nil {
50+
log.Error("Failed to get service from consul", "serviceID", serviceID, "err", err)
51+
return nil, err
52+
}
53+
54+
ethURLs := make([]string, len(list))
55+
for i, srv := range list {
56+
addr := srv.ServiceAddress
57+
if addr == "" {
58+
addr = srv.Address
59+
}
60+
ethURLs[i] = fmt.Sprintf("%s://%s:%d", serviceScheme, addr, srv.ServicePort)
61+
}
62+
return ethURLs, nil
63+
}
64+
65+
func createConsulClient(rawURL string) (*consulAPI.Client, error) {
66+
consulURL, err := netURL.Parse(rawURL)
67+
if err != nil {
68+
log.Error("Failed to parse consul url", "url", rawURL, "err", err)
69+
return nil, err
70+
}
71+
72+
config := &consulAPI.Config{
73+
Address: consulURL.Host,
74+
Scheme: consulURL.Scheme,
75+
}
76+
77+
client, err := consulAPI.NewClient(config)
78+
if err != nil {
79+
log.Error("Failed to create consul client", "err", err)
80+
return nil, err
81+
}
82+
return client, nil
83+
}

0 commit comments

Comments
 (0)