Skip to content

Commit

Permalink
Accommodate Eureka version 1.22's format for ports
Browse files Browse the repository at this point in the history
In version 1.22, the Eureka server changed its JSON encoding for an
instance's port numbers: instead of writing the port numbers as JSON
string values, it started writing them as JSON number values. In order
to accommodate Eureka servers of versions on either side of this
change, accept instance port numbers as both strings and numbers, but
continue to emit them as strings, which Eureka servers of all versions
continue to accept.
  • Loading branch information
seh committed Jan 26, 2017
1 parent 9352b64 commit df904f6
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 70 deletions.
168 changes: 99 additions & 69 deletions marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,66 @@ package fargo
import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"strconv"
)

// Temporary structs used for GetAppsResponse unmarshalling
type getAppsResponseArray GetAppsResponse
type getAppsResponseSingle struct {
Application *Application `json:"application"`
AppsHashcode string `json:"apps__hashcode"`
VersionsDelta int `json:"versions__delta"`
func intFromJSONNumberOrString(jv interface{}, description string) (int, error) {
switch v := jv.(type) {
case float64:
return int(v), nil
case string:
n, err := strconv.Atoi(v)
if err != nil {
return 0, err
}
return n, nil
default:
return 0, fmt.Errorf("unexpected %s: %[2]v (type %[2]T)", description, jv)
}
}

// UnmarshalJSON is a custom JSON unmarshaler for GetAppsResponse to deal with
// sometimes non-wrapped Application arrays when there is only a single Application item.
func (r *GetAppsResponse) UnmarshalJSON(b []byte) error {
marshalLog.Debugf("GetAppsResponse.UnmarshalJSON b:%s\n", string(b))
var err error
resolveDelta := func(d interface{}) (int, error) {
return intFromJSONNumberOrString(d, "versions delta")
}

// Normal array case
var ra getAppsResponseArray
if err = json.Unmarshal(b, &ra); err == nil {
marshalLog.Debug("GetAppsResponse.UnmarshalJSON ra:%+v\n", ra)
*r = GetAppsResponse(ra)
return nil
type getAppsResponse GetAppsResponse
auxArray := struct {
*getAppsResponse
VersionsDelta interface{} `json:"versions__delta"`
}{
getAppsResponse: (*getAppsResponse)(r),
}
var err error
if err = json.Unmarshal(b, &auxArray); err == nil {
marshalLog.Debugf("GetAppsResponse.UnmarshalJSON array:%+v\n", auxArray)
r.VersionsDelta, err = resolveDelta(auxArray.VersionsDelta)
return err
}

// Bogus non-wrapped case
var rs getAppsResponseSingle
if err = json.Unmarshal(b, &rs); err == nil {
marshalLog.Debug("GetAppsResponse.UnmarshalJSON rs:%+v\n", rs)
r.Applications = make([]*Application, 1, 1)
r.Applications[0] = rs.Application
r.AppsHashcode = rs.AppsHashcode
r.VersionsDelta = rs.VersionsDelta
return nil
auxSingle := struct {
Application *Application `json:"application"`
AppsHashcode string `json:"apps__hashcode"`
VersionsDelta interface{} `json:"versions__delta"`
}{}
if err := json.Unmarshal(b, &auxSingle); err != nil {
return err
}
return err
marshalLog.Debugf("GetAppsResponse.UnmarshalJSON single:%+v\n", auxSingle)
if r.VersionsDelta, err = resolveDelta(auxSingle.VersionsDelta); err != nil {
return err
}
r.Applications = make([]*Application, 1, 1)
r.Applications[0] = auxSingle.Application
r.AppsHashcode = auxSingle.AppsHashcode
return nil
}

// Temporary structs used for Application unmarshalling
Expand All @@ -59,15 +83,15 @@ func (a *Application) UnmarshalJSON(b []byte) error {
// Normal array case
var aa applicationArray
if err = json.Unmarshal(b, &aa); err == nil {
marshalLog.Debug("Application.UnmarshalJSON aa:%+v\n", aa)
marshalLog.Debugf("Application.UnmarshalJSON aa:%+v\n", aa)
*a = Application(aa)
return nil
}

// Bogus non-wrapped case
var as applicationSingle
if err = json.Unmarshal(b, &as); err == nil {
marshalLog.Debug("Application.UnmarshalJSON as:%+v\n", as)
marshalLog.Debugf("Application.UnmarshalJSON as:%+v\n", as)
a.Name = as.Name
a.Instances = make([]*Instance, 1, 1)
a.Instances[0] = as.Instance
Expand All @@ -76,79 +100,85 @@ func (a *Application) UnmarshalJSON(b []byte) error {
return err
}

// jsonFormatPort describes an instance's network port, including whether its registrant considers
// the port to be enabled or disabled.
//
// Example JSON encoding:
//
// "port":{"@enabled":"true", "$":"7101"}
//
// Note that later versions of Eureka write the port number as a JSON number rather than as a
// decimal-formatted string.
type jsonFormatPort struct {
Number string `json:"$"`
Enabled string `json:"@enabled"`
}

func boolAsString(b bool) string {
if b {
return "true"
}
return "false"
}

func makeJSONFormatPort(port int, enabled bool) jsonFormatPort {
return jsonFormatPort{
strconv.Itoa(port),
boolAsString(enabled),
}
}

func parsePort(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
log.Warningf("Invalid port error: %s", err.Error())
}
return n
}

func stringAsBool(s string) bool {
return s == "true"
}

// UnmarshalJSON is a custom JSON unmarshaler for Instance, transcribing the two composite port
// specifications up to top-level fields.
func (i *Instance) UnmarshalJSON(b []byte) error {
// Preclude recursive calls to MarshalJSON.
type instance Instance
// inboundJSONFormatPort describes an instance's network port, including whether its registrant
// considers the port to be enabled or disabled.
//
// Example JSON encoding:
//
// Eureka versions 1.2.1 and prior:
// "port":{"@enabled":"true", "$":"7101"}
//
// Eureka version 1.2.2 and later:
// "port":{"@enabled":"true", "$":7101}
//
// Note that later versions of Eureka write the port number as a JSON number rather than as a
// decimal-formatted string. We accept it as either an integer or a string. Strangely, the
// "@enabled" field remains a string.
type inboundJSONFormatPort struct {
Number interface{} `json:"$"`
Enabled bool `json:"@enabled,string"`
}
aux := struct {
*instance
Port jsonFormatPort `json:"port"`
SecurePort jsonFormatPort `json:"securePort"`
Port inboundJSONFormatPort `json:"port"`
SecurePort inboundJSONFormatPort `json:"securePort"`
}{
instance: (*instance)(i),
}
if err := json.Unmarshal(b, &aux); err != nil {
return err
}
i.Port = parsePort(aux.Port.Number)
i.PortEnabled = stringAsBool(aux.Port.Enabled)
i.SecurePort = parsePort(aux.SecurePort.Number)
i.SecurePortEnabled = stringAsBool(aux.SecurePort.Enabled)
resolvePort := func(port interface{}) (int, error) {
return intFromJSONNumberOrString(port, "port number")
}
var err error
if i.Port, err = resolvePort(aux.Port.Number); err != nil {
return err
}
i.PortEnabled = aux.Port.Enabled
if i.SecurePort, err = resolvePort(aux.SecurePort.Number); err != nil {
return err
}
i.SecurePortEnabled = aux.SecurePort.Enabled
return nil
}

// MarshalJSON is a custom JSON marshaler for Instance, adapting the top-level raw port values to
// the composite port specifications.
func (i *Instance) MarshalJSON() ([]byte, error) {
// Preclude recursive calls to MarshalJSON.
type instance Instance
// outboundJSONFormatPort describes an instance's network port, including whether its registrant
// considers the port to be enabled or disabled.
//
// Example JSON encoding:
//
// "port":{"@enabled":"true", "$":"7101"}
//
// Note that later versions of Eureka write the port number as a JSON number rather than as a
// decimal-formatted string. We emit the port number as a string, not knowing the Eureka
// server's version. Strangely, the "@enabled" field remains a string.
type outboundJSONFormatPort struct {
Number int `json:"$,string"`
Enabled bool `json:"@enabled,string"`
}
aux := struct {
*instance
Port jsonFormatPort `json:"port"`
SecurePort jsonFormatPort `json:"securePort"`
Port outboundJSONFormatPort `json:"port"`
SecurePort outboundJSONFormatPort `json:"securePort"`
}{
(*instance)(i),
makeJSONFormatPort(i.Port, i.PortEnabled),
makeJSONFormatPort(i.SecurePort, i.SecurePortEnabled),
outboundJSONFormatPort{i.Port, i.PortEnabled},
outboundJSONFormatPort{i.SecurePort, i.SecurePortEnabled},
}
return json.Marshal(&aux)
}
Expand Down
49 changes: 49 additions & 0 deletions tests/marshal_sample/apps-sample-1-1-post-v122.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"applications":{
"versions__delta":"1",
"apps__hashcode":"UP_24_",
"application":[
{
"name":"TESTENG.METRICSD",
"instance":{
"hostName":"192.168.40.94:7101",
"app":"TESTENG.METRICSD",
"ipAddr":"192.168.40.94",
"status":"UP",
"overriddenstatus":"UP",
"port":{
"@enabled":"true",
"$":7101
},
"securePort":{
"@enabled":"true",
"$":7101
},
"countryId":1,
"dataCenterInfo":{
"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
"name":"MyOwn"
},
"leaseInfo":{
"renewalIntervalInSecs":30,
"durationInSecs":90,
"registrationTimestamp":1402450388650,
"lastRenewalTimestamp":1402552694257,
"evictionTimestamp":0,
"serviceUpTimestamp":1402450388774
},
"metadata":{
"service-query-uri":"https://192.168.40.94:7101",
"base-uri":"https://192.168.40.94:7101",
"swagger-api-docs":"https://192.168.40.94:7101/api-docs"
},
"vipAddress":"[]",
"isCoordinatingDiscoveryServer":false,
"lastUpdatedTimestamp":1402450388774,
"lastDirtyTimestamp":1402450388774,
"actionType":"MODIFIED"
}
}
]
}
}
17 changes: 16 additions & 1 deletion tests/marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fargo_test
// MIT Licensed (see README.md) - Copyright (c) 2013 Hudl <@Hudl>

import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
Expand Down Expand Up @@ -58,7 +59,7 @@ func portsEqual(actual, expected *fargo.Instance) {
}

func jsonEncodedInstanceHasPortsEqualTo(b []byte, expected *fargo.Instance) {
Convey("And reading them back should yield the equivalent value", func() {
Convey("Reading them back should yield the equivalent value", func() {
var decoded fargo.Instance
err := json.Unmarshal(b, &decoded)
So(err, ShouldBeNil)
Expand Down Expand Up @@ -92,6 +93,10 @@ func TestPortsMarshal(t *testing.T) {
So(s, ShouldContainSubstring, `,"securePort":{"$":"0","@enabled":"false"}`)

jsonEncodedInstanceHasPortsEqualTo(b, &ins)

Convey("When the Eureka server is version 1.22 or later", func() {
jsonEncodedInstanceHasPortsEqualTo(bytes.Replace(b, []byte(`"80"`), []byte("80"), -1), &ins)
})
})
})

Expand Down Expand Up @@ -124,6 +129,10 @@ func TestPortsMarshal(t *testing.T) {
So(s, ShouldContainSubstring, `,"securePort":{"$":"443","@enabled":"true"}`)

jsonEncodedInstanceHasPortsEqualTo(b, &ins)

Convey("When the Eureka server is version 1.22 or later", func() {
jsonEncodedInstanceHasPortsEqualTo(bytes.Replace(b, []byte(`"443"`), []byte("443"), -1), &ins)
})
})
})

Expand Down Expand Up @@ -158,6 +167,12 @@ func TestPortsMarshal(t *testing.T) {
So(s, ShouldContainSubstring, `,"securePort":{"$":"443","@enabled":"true"}`)

jsonEncodedInstanceHasPortsEqualTo(b, &ins)

Convey("When the Eureka server is version 1.22 or later", func() {
b = bytes.Replace(b, []byte(`"80"`), []byte("80"), -1)
b = bytes.Replace(b, []byte(`"443"`), []byte("443"), -1)
jsonEncodedInstanceHasPortsEqualTo(b, &ins)
})
})
})

Expand Down
4 changes: 4 additions & 0 deletions tests/net_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ func TestRegistration(t *testing.T) {
i := fargo.Instance{
HostName: "i-123456",
Port: 9090,
PortEnabled: true,
App: "TESTAPP",
IPAddr: "127.0.0.10",
VipAddress: "127.0.0.10",
Expand All @@ -231,6 +232,7 @@ func TestRegistration(t *testing.T) {
j := fargo.Instance{
HostName: "i-6543",
Port: 9090,
PortEnabled: true,
App: "TESTAPP",
IPAddr: "127.0.0.10",
VipAddress: "127.0.0.10",
Expand Down Expand Up @@ -264,6 +266,7 @@ func TestReregistration(t *testing.T) {
i := fargo.Instance{
HostName: "i-123456",
Port: 9090,
PortEnabled: true,
App: "TESTAPP",
IPAddr: "127.0.0.10",
VipAddress: "127.0.0.10",
Expand Down Expand Up @@ -305,6 +308,7 @@ func DontTestDeregistration(t *testing.T) {
i := fargo.Instance{
HostName: "i-123456",
Port: 9090,
PortEnabled: true,
App: "TESTAPP",
IPAddr: "127.0.0.10",
VipAddress: "127.0.0.10",
Expand Down

0 comments on commit df904f6

Please sign in to comment.