Codebase list golang-github-go-kit-kit / cb03da6
Add Eureka service discovery implementation This commit adds a service discovery implementation for the Eureka registry, a component in Netflix's OSS suite. Eureka is a popular choice in JVM-based microservice architectures, particularly when used in conjunction with the Spring Cloud ecosystem. This implementation delegates integration to Fargo: the de facto Golang Eureka client. It allows the user to employ a Fargo connection as the foundational configuration item for Registrars and Subscribers. This should offer the user the most control of Fargo within the constraints of the Go-kit service discovery abstractions. Martin Baillie 6 years ago
10 changed file(s) with 678 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
2323 ETCD_ADDR: http://localhost:2379
2424 CONSUL_ADDR: localhost:8500
2525 ZK_ADDR: localhost:2181
26 EUREKA_ADDR: http://localhost:8761/eureka
1313 image: zookeeper
1414 ports:
1515 - "2181:2181"
16 eureka:
17 image: springcloud/eureka
18 environment:
19 eureka.server.responseCacheUpdateIntervalMs: 1000
20 ports:
21 - "8761:8761"
0 package eureka
1
2 import (
3 stdeureka "github.com/hudl/fargo"
4 stdeurekalogging "github.com/op/go-logging"
5 )
6
7 func init() {
8 // Quieten Fargo's own logging
9 stdeurekalogging.SetLevel(stdeurekalogging.ERROR, "fargo")
10 }
11
12 // Client is a wrapper around the Eureka API.
13 type Client interface {
14 // Register an instance with Eureka.
15 Register(i *stdeureka.Instance) error
16
17 // Deregister an instance from Eureka.
18 Deregister(i *stdeureka.Instance) error
19
20 // Send an instance heartbeat to Eureka.
21 Heartbeat(i *stdeureka.Instance) error
22
23 // Get all instances for an app in Eureka.
24 Instances(app string) ([]*stdeureka.Instance, error)
25
26 // Receive scheduled updates about an app's instances in Eureka.
27 ScheduleUpdates(app string, quitc chan struct{}) <-chan stdeureka.AppUpdate
28 }
29
30 type client struct {
31 connection *stdeureka.EurekaConnection
32 }
33
34 // NewClient returns an implementation of the Client interface, wrapping a
35 // concrete connection to Eureka using the Fargo library.
36 // Taking in Fargo's own connection abstraction gives the user maximum
37 // freedom in regards to how that connection is configured.
38 func NewClient(ec *stdeureka.EurekaConnection) Client {
39 return &client{connection: ec}
40 }
41
42 func (c *client) Register(i *stdeureka.Instance) error {
43 if c.instanceRegistered(i) {
44 // Already registered. Send a heartbeat instead.
45 return c.Heartbeat(i)
46 }
47 return c.connection.RegisterInstance(i)
48 }
49
50 func (c *client) Deregister(i *stdeureka.Instance) error {
51 return c.connection.DeregisterInstance(i)
52 }
53
54 func (c *client) Heartbeat(i *stdeureka.Instance) (err error) {
55 if err = c.connection.HeartBeatInstance(i); err != nil && c.instanceNotFoundErr(err) {
56 // Instance not registered. Register first before sending heartbeats.
57 return c.Register(i)
58 }
59 return err
60 }
61
62 func (c *client) Instances(app string) ([]*stdeureka.Instance, error) {
63 stdApp, err := c.connection.GetApp(app)
64 if err != nil {
65 return nil, err
66 }
67 return stdApp.Instances, nil
68 }
69
70 func (c *client) ScheduleUpdates(app string, quitc chan struct{}) <-chan stdeureka.AppUpdate {
71 return c.connection.ScheduleAppUpdates(app, false, quitc)
72 }
73
74 func (c *client) instanceRegistered(i *stdeureka.Instance) bool {
75 _, err := c.connection.GetInstance(i.App, i.Id())
76 return err == nil
77 }
78
79 func (c *client) instanceNotFoundErr(err error) bool {
80 code, ok := stdeureka.HTTPResponseStatusCode(err)
81 return ok && code == 404
82 }
0 package eureka
1
2 import (
3 "errors"
4 "reflect"
5
6 "github.com/go-kit/kit/log"
7 stdeureka "github.com/hudl/fargo"
8 )
9
10 var (
11 errTest = errors.New("kaboom")
12 loggerTest = log.NewNopLogger()
13 instanceTest1 = &stdeureka.Instance{
14 HostName: "server1.acme.org",
15 Port: 8080,
16 App: "go-kit",
17 IPAddr: "192.168.0.1",
18 VipAddress: "192.168.0.1",
19 SecureVipAddress: "192.168.0.1",
20 HealthCheckUrl: "http://server1.acme.org:8080/healthz",
21 StatusPageUrl: "http://server1.acme.org:8080/status",
22 HomePageUrl: "http://server1.acme.org:8080/",
23 Status: stdeureka.UP,
24 DataCenterInfo: stdeureka.DataCenterInfo{Name: stdeureka.MyOwn},
25 LeaseInfo: stdeureka.LeaseInfo{RenewalIntervalInSecs: 1},
26 }
27 instanceTest2 = &stdeureka.Instance{
28 HostName: "server2.acme.org",
29 Port: 8080,
30 App: "go-kit",
31 IPAddr: "192.168.0.2",
32 VipAddress: "192.168.0.2",
33 SecureVipAddress: "192.168.0.2",
34 HealthCheckUrl: "http://server2.acme.org:8080/healthz",
35 StatusPageUrl: "http://server2.acme.org:8080/status",
36 HomePageUrl: "http://server2.acme.org:8080/",
37 Status: stdeureka.UP,
38 DataCenterInfo: stdeureka.DataCenterInfo{Name: stdeureka.MyOwn},
39 }
40 applicationTest = &stdeureka.Application{
41 Name: "go-kit",
42 Instances: []*stdeureka.Instance{instanceTest1, instanceTest2},
43 }
44 )
45
46 type testClient struct {
47 instances []*stdeureka.Instance
48 application *stdeureka.Application
49 errInstances error
50 errApplication error
51 errHeartbeat error
52 }
53
54 func (c *testClient) Register(i *stdeureka.Instance) error {
55 for _, instance := range c.instances {
56 if reflect.DeepEqual(*instance, *i) {
57 return errors.New("already registered")
58 }
59 }
60
61 c.instances = append(c.instances, i)
62 return nil
63 }
64
65 func (c *testClient) Deregister(i *stdeureka.Instance) error {
66 var newInstances []*stdeureka.Instance
67 for _, instance := range c.instances {
68 if reflect.DeepEqual(*instance, *i) {
69 continue
70 }
71 newInstances = append(newInstances, instance)
72 }
73 if len(newInstances) == len(c.instances) {
74 return errors.New("not registered")
75 }
76
77 c.instances = newInstances
78 return nil
79 }
80
81 func (c *testClient) Heartbeat(i *stdeureka.Instance) (err error) {
82 return c.errHeartbeat
83 }
84
85 func (c *testClient) Instances(app string) ([]*stdeureka.Instance, error) {
86 return c.instances, c.errInstances
87 }
88
89 func (c *testClient) ScheduleUpdates(service string, quitc chan struct{}) <-chan stdeureka.AppUpdate {
90 updatec := make(chan stdeureka.AppUpdate, 1)
91 updatec <- stdeureka.AppUpdate{App: c.application, Err: c.errApplication}
92 return updatec
93 }
0 // Package eureka provides subscriber and registrar implementations for Netflix OSS's Eureka
1 package eureka
0 // +build integration
1
2 package eureka
3
4 import (
5 "io"
6 "os"
7 "testing"
8 "time"
9
10 "github.com/go-kit/kit/endpoint"
11 "github.com/go-kit/kit/log"
12 stdeureka "github.com/hudl/fargo"
13 )
14
15 // Package sd/eureka provides a wrapper around the Netflix Eureka service
16 // registry by way of the Fargo library. This test assumes the user has an
17 // instance of Eureka available at the address in the environment variable.
18 // Example `${EUREKA_ADDR}` format: http://localhost:8761/eureka
19 //
20 // NOTE: when starting a Eureka server for integration testing, ensure
21 // the response cache interval is reduced to one second. This can be
22 // achieved with the following Java argument:
23 // `-Deureka.server.responseCacheUpdateIntervalMs=1000`
24 func TestIntegration(t *testing.T) {
25 eurekaAddr := os.Getenv("EUREKA_ADDR")
26 if eurekaAddr == "" {
27 t.Skip("EUREKA_ADDR is not set")
28 }
29
30 var client Client
31 {
32 var stdConfig stdeureka.Config
33 stdConfig.Eureka.ServiceUrls = []string{eurekaAddr}
34 stdConfig.Eureka.PollIntervalSeconds = 1
35
36 stdConnection := stdeureka.NewConnFromConfig(stdConfig)
37 client = NewClient(&stdConnection)
38 }
39
40 logger := log.NewLogfmtLogger(os.Stderr)
41 logger = log.With(logger, "ts", log.DefaultTimestamp)
42
43 // Register one instance.
44 registrar1 := NewRegistrar(client, instanceTest1, log.With(logger, "component", "registrar1"))
45 registrar1.Register()
46 defer registrar1.Deregister()
47
48 // This should be enough time for the Eureka server response cache to update.
49 time.Sleep(time.Second)
50
51 // Build a subscriber.
52 factory := func(instance string) (endpoint.Endpoint, io.Closer, error) {
53 t.Logf("factory invoked for %q", instance)
54 return endpoint.Nop, nil, nil
55 }
56 s := NewSubscriber(
57 client,
58 factory,
59 log.With(logger, "component", "subscriber"),
60 instanceTest1.App,
61 )
62 defer s.Stop()
63
64 // We should have one endpoint immediately after subscriber instantiation.
65 endpoints, err := s.Endpoints()
66 if err != nil {
67 t.Error(err)
68 }
69 if want, have := 1, len(endpoints); want != have {
70 t.Errorf("want %d, have %d", want, have)
71 }
72
73 // Register a second instance
74 registrar2 := NewRegistrar(client, instanceTest2, log.With(logger, "component", "registrar2"))
75 registrar2.Register()
76 defer registrar2.Deregister() // In case of exceptional circumstances.
77
78 // This should be enough time for a scheduled update assuming Eureka is
79 // configured with the properties mentioned in the function comments.
80 time.Sleep(2 * time.Second)
81
82 // Now we should have two endpoints.
83 endpoints, err = s.Endpoints()
84 if err != nil {
85 t.Error(err)
86 }
87 if want, have := 2, len(endpoints); want != have {
88 t.Errorf("want %d, have %d", want, have)
89 }
90
91 // Deregister the second instance.
92 registrar2.Deregister()
93
94 // Wait for another scheduled update.
95 time.Sleep(2 * time.Second)
96
97 // And then there was one.
98 endpoints, err = s.Endpoints()
99 if err != nil {
100 t.Error(err)
101 }
102 if want, have := 1, len(endpoints); want != have {
103 t.Errorf("want %d, have %d", want, have)
104 }
105 }
0 package eureka
1
2 import (
3 "fmt"
4 "sync"
5 "time"
6
7 stdeureka "github.com/hudl/fargo"
8
9 "github.com/go-kit/kit/log"
10 )
11
12 // Registrar maintains service instance liveness information in Eureka.
13 type Registrar struct {
14 client Client
15 instance *stdeureka.Instance
16 logger log.Logger
17
18 quitmtx sync.Mutex
19 quit chan bool
20 }
21
22 // NewRegistrar returns an Eureka Registrar acting on behalf of the provided
23 // Fargo instance.
24 func NewRegistrar(client Client, i *stdeureka.Instance, l log.Logger) *Registrar {
25 return &Registrar{
26 client: client,
27 instance: i,
28 logger: log.With(l, "service", i.App, "address", fmt.Sprintf("%s:%d", i.IPAddr, i.Port)),
29 }
30 }
31
32 // Register implements sd.Registrar interface.
33 func (r *Registrar) Register() {
34 if err := r.client.Register(r.instance); err != nil {
35 r.logger.Log("err", err)
36 } else {
37 r.logger.Log("action", "register")
38 }
39
40 if r.instance.LeaseInfo.RenewalIntervalInSecs > 0 {
41 // User has opted for heartbeat functionality in Eureka.
42 go r.loop()
43 }
44 }
45
46 // Deregister implements sd.Registrar interface.
47 func (r *Registrar) Deregister() {
48 if err := r.client.Deregister(r.instance); err != nil {
49 r.logger.Log("err", err)
50 } else {
51 r.logger.Log("action", "deregister")
52 }
53
54 r.quitmtx.Lock()
55 defer r.quitmtx.Unlock()
56 if r.quit != nil {
57 r.quit <- true
58 }
59 }
60
61 func (r *Registrar) loop() {
62 r.quitmtx.Lock()
63 if r.quit != nil {
64 defer r.quitmtx.Unlock()
65 return // Already running.
66 }
67 r.quit = make(chan bool)
68 r.quitmtx.Unlock()
69
70 tick := time.NewTicker(time.Duration(r.instance.LeaseInfo.RenewalIntervalInSecs) * time.Second)
71 defer tick.Stop()
72 for {
73 select {
74 case <-tick.C:
75 if err := r.client.Heartbeat(r.instance); err != nil {
76 r.logger.Log("err", err)
77 }
78 case <-r.quit:
79 r.quitmtx.Lock()
80 defer r.quitmtx.Unlock()
81
82 close(r.quit)
83 r.quit = nil
84
85 return
86 }
87 }
88 }
0 package eureka
1
2 import (
3 "testing"
4 "time"
5
6 stdeureka "github.com/hudl/fargo"
7 )
8
9 func TestRegistrar(t *testing.T) {
10 client := &testClient{
11 instances: []*stdeureka.Instance{},
12 errHeartbeat: errTest,
13 }
14
15 r := NewRegistrar(client, instanceTest1, loggerTest)
16 if want, have := 0, len(client.instances); want != have {
17 t.Errorf("want %d, have %d", want, have)
18 }
19
20 // Not registered.
21 r.Deregister()
22 if want, have := 0, len(client.instances); want != have {
23 t.Errorf("want %d, have %d", want, have)
24 }
25
26 // Register.
27 r.Register()
28 if want, have := 1, len(client.instances); want != have {
29 t.Errorf("want %d, have %d", want, have)
30 }
31
32 // Deregister.
33 r.Deregister()
34 if want, have := 0, len(client.instances); want != have {
35 t.Errorf("want %d, have %d", want, have)
36 }
37
38 // Already registered.
39 r.Register()
40 if want, have := 1, len(client.instances); want != have {
41 t.Errorf("want %d, have %d", want, have)
42 }
43 r.Register()
44 if want, have := 1, len(client.instances); want != have {
45 t.Errorf("want %d, have %d", want, have)
46 }
47
48 // Wait for a heartbeat failure.
49 time.Sleep(time.Second)
50 if want, have := 1, len(client.instances); want != have {
51 t.Errorf("want %d, have %d", want, have)
52 }
53 r.Deregister()
54 if want, have := 0, len(client.instances); want != have {
55 t.Errorf("want %d, have %d", want, have)
56 }
57 }
0 package eureka
1
2 import (
3 "fmt"
4
5 stdeureka "github.com/hudl/fargo"
6
7 "github.com/go-kit/kit/endpoint"
8 "github.com/go-kit/kit/log"
9 "github.com/go-kit/kit/sd"
10 "github.com/go-kit/kit/sd/cache"
11 )
12
13 // Subscriber yields endpoints stored in the Eureka registry for the given app.
14 // Changes in that app are watched and will update the Subscriber endpoints.
15 type Subscriber struct {
16 client Client
17 cache *cache.Cache
18 logger log.Logger
19 app string
20 quitc chan struct{}
21 }
22
23 var _ sd.Subscriber = &Subscriber{}
24
25 // NewSubscriber returns a Eureka subscriber. It will start watching the given
26 // app string for changes, and update the endpoints accordingly.
27 func NewSubscriber(c Client, f sd.Factory, l log.Logger, app string) *Subscriber {
28 s := &Subscriber{
29 client: c,
30 cache: cache.New(f, l),
31 app: app,
32 logger: l,
33 quitc: make(chan struct{}),
34 }
35
36 instances, err := s.getInstances()
37 if err == nil {
38 s.logger.Log("app", s.app, "instances", len(instances))
39 } else {
40 s.logger.Log("app", s.app, "msg", "failed to retrieve instances", "err", err)
41 }
42
43 s.cache.Update(instances)
44 go s.loop()
45 return s
46 }
47
48 func (s *Subscriber) getInstances() ([]string, error) {
49 stdInstances, err := s.client.Instances(s.app)
50 if err != nil {
51 return nil, err
52 }
53 return convertStdInstances(stdInstances), nil
54 }
55
56 func (s *Subscriber) loop() {
57 updatec := s.client.ScheduleUpdates(s.app, s.quitc)
58 for {
59 select {
60 case <-s.quitc:
61 return
62 case u := <-updatec:
63 if u.Err != nil {
64 s.logger.Log("app", s.app, "msg", "failed to retrieve instances", "err", u.Err)
65 continue
66 }
67
68 instances := convertStdApplication(u.App)
69 s.logger.Log("app", s.app, "instances", len(instances))
70 s.cache.Update(instances)
71 }
72 }
73 }
74
75 // Endpoints implements the Subscriber interface.
76 func (s *Subscriber) Endpoints() ([]endpoint.Endpoint, error) {
77 return s.cache.Endpoints(), nil
78 }
79
80 // Stop terminates the Subscriber.
81 func (s *Subscriber) Stop() {
82 close(s.quitc)
83 }
84
85 func convertStdApplication(stdApplication *stdeureka.Application) (instances []string) {
86 if stdApplication != nil {
87 instances = convertStdInstances(stdApplication.Instances)
88 }
89 return instances
90 }
91
92 func convertStdInstances(stdInstances []*stdeureka.Instance) []string {
93 instances := make([]string, len(stdInstances))
94 for i, stdInstance := range stdInstances {
95 instances[i] = fmt.Sprintf("%s:%d", stdInstance.IPAddr, stdInstance.Port)
96 }
97 return instances
98 }
0 package eureka
1
2 import (
3 "io"
4 "testing"
5 "time"
6
7 "github.com/go-kit/kit/endpoint"
8 stdeureka "github.com/hudl/fargo"
9 )
10
11 func TestSubscriber(t *testing.T) {
12 factory := func(string) (endpoint.Endpoint, io.Closer, error) {
13 return endpoint.Nop, nil, nil
14 }
15
16 client := &testClient{
17 instances: []*stdeureka.Instance{instanceTest1},
18 application: applicationTest,
19 errApplication: nil,
20 }
21
22 s := NewSubscriber(client, factory, loggerTest, instanceTest1.App)
23 defer s.Stop()
24
25 endpoints, err := s.Endpoints()
26 if err != nil {
27 t.Fatal(err)
28 }
29
30 if want, have := 1, len(endpoints); want != have {
31 t.Errorf("want %d, have %d", want, have)
32 }
33 }
34
35 func TestSubscriberScheduleUpdates(t *testing.T) {
36 factory := func(string) (endpoint.Endpoint, io.Closer, error) {
37 return endpoint.Nop, nil, nil
38 }
39
40 client := &testClient{
41 instances: []*stdeureka.Instance{instanceTest1},
42 application: applicationTest,
43 errApplication: nil,
44 }
45
46 s := NewSubscriber(client, factory, loggerTest, instanceTest1.App)
47 defer s.Stop()
48
49 endpoints, _ := s.Endpoints()
50 if want, have := 1, len(endpoints); want != have {
51 t.Errorf("want %d, have %d", want, have)
52 }
53
54 time.Sleep(50 * time.Millisecond)
55
56 endpoints, _ = s.Endpoints()
57 if want, have := 2, len(endpoints); want != have {
58 t.Errorf("want %v, have %v", want, have)
59 }
60 }
61
62 func TestBadFactory(t *testing.T) {
63 factory := func(string) (endpoint.Endpoint, io.Closer, error) {
64 return nil, nil, errTest
65 }
66
67 client := &testClient{
68 instances: []*stdeureka.Instance{instanceTest1},
69 }
70
71 s := NewSubscriber(client, factory, loggerTest, instanceTest1.App)
72 defer s.Stop()
73
74 endpoints, err := s.Endpoints()
75 if err != nil {
76 t.Fatal(err)
77 }
78
79 if want, have := 0, len(endpoints); want != have {
80 t.Errorf("want %d, have %d", want, have)
81 }
82 }
83
84 func TestBadSubscriberInstances(t *testing.T) {
85 factory := func(string) (endpoint.Endpoint, io.Closer, error) {
86 return endpoint.Nop, nil, nil
87 }
88
89 client := &testClient{
90 errInstances: errTest,
91 application: applicationTest,
92 errApplication: nil,
93 }
94
95 s := NewSubscriber(client, factory, loggerTest, instanceTest1.App)
96 defer s.Stop()
97
98 endpoints, err := s.Endpoints()
99 if err != nil {
100 t.Fatal(err)
101 }
102
103 if want, have := 0, len(endpoints); want != have {
104 t.Errorf("want %d, have %d", want, have)
105 }
106 }
107
108 func TestBadSubscriberScheduleUpdates(t *testing.T) {
109 factory := func(string) (endpoint.Endpoint, io.Closer, error) {
110 return endpoint.Nop, nil, nil
111 }
112
113 client := &testClient{
114 instances: []*stdeureka.Instance{instanceTest1},
115 application: applicationTest,
116 errApplication: errTest,
117 }
118
119 s := NewSubscriber(client, factory, loggerTest, instanceTest1.App)
120 defer s.Stop()
121
122 endpoints, err := s.Endpoints()
123 if err != nil {
124 t.Error(err)
125 }
126 if want, have := 1, len(endpoints); want != have {
127 t.Errorf("want %d, have %d", want, have)
128 }
129
130 time.Sleep(50 * time.Millisecond)
131
132 endpoints, err = s.Endpoints()
133 if err != nil {
134 t.Error(err)
135 }
136 if want, have := 1, len(endpoints); want != have {
137 t.Errorf("want %v, have %v", want, have)
138 }
139 }