-
Notifications
You must be signed in to change notification settings - Fork 53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Preserve an instance's enabled status for its two ports #59
Conversation
61081f6
to
d765ac6
Compare
When transcribing the fargo.Instance struct to and from both JSON and XML, indicate and retain whether each port is enabled or not. Note that in the Java Eureka library, by default the insecure port is enabled and the secure port is disabled. Here the zero value of a fargo.Instance has the insecure port disabled as well. While it would be possible to invert the sense of the corresponding field to indicate whether the insecure port is disabled, so that the zero value of false matches the Java library's behavior, the resulting asymmetry with the secure port's field is too awkward. Instead, require registrants to enable the ports explicitly. This change removes the exported fargo.Port type and several exported fields of the fargo.Instance type: XMLName PortJ SecurePortJ None of these were exposed deliberately for use by callers; they were all exposed only as accidental consequences of the JSON and XML marshalling library's idiosyncrasies.
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.
As of Eureka version 1.2.2 and later, when registering an instance encoded as JSON, one must supply a value for the instance.dataCenterInfo.@Class field, which must be set appropriately for the sibling "name" field's value. Take care of setting this value for the "Amazon" and "MyOwn" data center types, but allow callers to specify the value for their own custom data center types for which we cannot anticipate a suitable value.
When the Eureka server encodes an instance as JSON, it will sometimes write the data center metadata values that look like numbers as JSON numbers, rather than writing all metadata values as strings. Demanding that all metadata values arrive as JSON strings precludes proper decoding of instances, so be more flexible by first decoding the metadata values as any JSON type, and then projecting the values that are not strings back to strings. Note that we only project decoded Go types bool, float64, and nil; while it's possible that Go can decode a value of type []interface{} or map[string]interface{}, we make no effort to project those values.
Some tests were ignoring errors and proceeding to inspect query results, causing failure via panic. Instead, fail these tests via more explicit assertions.
d765ac6
to
f1f7c88
Compare
I had forgotten to resolve the conflict here after #60 merged. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally I would like to have second reviewer on that, but from my point of view it all looks ok.
VersionsDelta int `json:"versions__delta"` | ||
func intFromJSONNumberOrString(jv interface{}, description string) (int, error) { | ||
switch v := jv.(type) { | ||
case float64: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this float64 and not int? Have you checked it's coming as a fload? Any thoughts of adding inte there as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per the documentation for json.Unmarshal
, numbers coming from JSON into Go are always of type float64
—barring use of json.(*Decoder).UseNumber
. (That's implemented here.) As it stands, fargo does not set this option on a Decoder
when decoding a fargo.GetAppsResponse
.
SecurePort int `xml:"securePort" json:"-"` | ||
SecurePortJ Port `json:"securePort" xml:"-"` | ||
Port int `xml:"-" json:"-"` | ||
PortEnabled bool `xml:"-" json:"-"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can we be so sure that PortJ has not been used by library-consuemers. What would be the downside of leaving PortJ
here so it's backwards compatible?
Are we planning major version update with this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought I had written about this earlier, but apparently not. I apologize for the delay in addressing your concern.
The message for commit 9352b64 in this patch series mentions this removal:
None of these were exposed deliberately for use by callers; they were
all exposed only as accidental consequences of the JSON and XML
marshalling library's idiosyncrasies.
We can't be sure whether anyone wrote to or read the "PortJ" and "SecurePortJ" fields. Removing them introduces an incompatibility, in the sense that code that used to compile will now fail to compile. I could add the fields back in, though the values read from those fields would be different than they were before, and writing to those fields wouldn't have the intended effect.
I had first tried to solve this problem by making use of those fields as they are, transferring values between them and making guesses as to whether setting a port to a nonzero value meant that it should be enabled, but I gave up on the idea and backed out those changes. As I wrote tests, I found that I couldn't even convince myself reliably of how it should work. It felt like we were going to just barely make it possible to register or detect which ports are enabled, but leave it too easy to get it wrong.
I am not sure that it's any more fair to our clients to allow their code to continue to compile as it did, but not work properly at run time. One thing I considered doing—and will do, if you think it's better than not doing it—is to leave those fields there, and when we read an Instance
from the Eureka server to transcribe the port number and whether it's enabled into those 'J' fields, duplicating the information from the "Port," "PortEnabled," "SecurePort," and "SecurePortEnabled" fields. However, I would not want to try something different in reverse: namely, I do not want to try to merge values from these different fields when marshaling an Instance
to send to the Eureka server. To do so introduces too much confusion for callers: What happens if the values conflict? Which one do I need to set? Do I need to set both?
I doubted that fargo had any clients accessing these fields profitably, but I did find one public use over in stv3v/cfkit. There is also st3v/pcf-micro-examples, but then I realized that one is a vendored copy of fargo. The former has a legitimate use. @st3v is the author of the commit that introduced those lines on 1 October 2015. (Note that that setting of the "PortJ" and "SecurePortJ" fields would not have any effect if EurekaConnection.UseJson
had been false instead.)
The st3v/cfkit project has fargo's version pinned via Godeps at commit ac37d05, which is from 10 September 2015. Hence, that project wouldn't be broken by this change unless it tried to advance its dependency version without changing its calling code.
@st3v Do you have an opinion on this matter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seh Don't worry about cfkit and pcf-micro-examples. Those were pet projects I played around with quite a while ago. They aren't used anywhere.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little bit confused here. Previously we were unmarshaling here with *r = GetAppsResponse(ra)
but now it seams to me that we are only unmarshaling VersionsDelta here?
Also minor, what does the auxArray stand for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note line 42, where we populate the "auxArray" variable's "getAppsResponse" (anonymous|embedded) field with variable "r." When we supply "auxArray' to json.Unmarshal
at line 45, it will write directly to a "r" (dereferencing the pointer as necessary), which is analogous to the previous assignment you recall. It will write to "r" and also write into the auxiliary struct's "VersionsDelta" field, which we then convert and assign back into "r"'s "VersionsDelta" field. The outer struct's field (of type interface{}
) shadows the field with the same name in the embedded "getAppsResponse" field (of type int
).
Here the variable name "auxArray" means "auxiliary structure for the "array of applications" case, versus the "single application" case handled at line 52. The variable name "aux" comes up frequently with this encoding or decoding technique in Go, where one introduces some separate, temporary, and only locally significant struct to handle the finicky details that Go's struct tags can't accommodate.
@@ -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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thx for finding that ⭐️
// | ||
// 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a thinking exercise. How hard will it be to detect eureka version and serve what's the most appropriate version of date depending on version. Will that be even cleaner? Are we worried that eureka might stop accepting old version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The server doesn't send anything indicative of its version—not even in an HTTP header. If we had already received some responses from a server and been able to inspect the fields that differ, we could then remember which "style" or "version" of the encoding we've seen the server use and then mimic that in our future requests. There's still that problem of how our client starts that conversation safely.
As for whether Eureka will stop accepting the old version, I think they'd have to introduce some new resource paths in order to be fair to all the possible clients out there of various versions. My impression is that their handling of the encoding here has been haphazard; it came down to whatever XStream happened to do, and the Java clients could still talk well enough with the servers, so it doesn't appear that they cared enough to document or even prevent these format changes. I don't think they were deliberate.
IPAddr string `xml:"ipAddr" json:"ipAddr"` | ||
VipAddress string `xml:"vipAddress" json:"vipAddress"` | ||
SecureVipAddress string `xml:"secureVipAddress" json:"secureVipAddress"` | ||
HostName string `xml:"hostName" json:"hostName"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why have you decided to remove XMLName here? What if it's used by someone?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When using an xml.Encoder
to encode an element, one must supply an xml.StartElement
that determines the element's name. The "XMLName" field has no effect in that case. I struggled with a bug here for a while, trying to figure out why my "XMLName" field's value wasn't being honored. I finally realized that the value passed to xml.(*Encoder).EncodeElement
is what determines the element name.
If someone was setting this field's value to something other than "instance," the Eureka server would have rejected the resulting message body. If they were to try to set it now—if I restored the field to the fargo.Instance
struct —it would have no effect. To leave it in would confuse both our maintainers and our users.
Now, it's possible that someone's code would stop compiling if they were accessing the "XMLName" field of fargo.Instance
. I didn't think it was worth retaining this cruft just to preserve that illusion of compatibility. I think that this does warrant bumping at least the minor version number in the next release that would contain these changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a great explanation! Thank you for taking your time to answer to all of my concerns in so much of a detail!
Let's go with the solution as it is right now. One thing, which is bothering me still is that we should try to write really short note (maybe just links to the discussion we were having here) what has changed and how to migrate when someone's code doesn't compile.
I'm happy with those changes and I'm leaning to merge this as it is right now.
This will be a major version bump since there are some breaking changes, with a note that migration ought to be painless.
Thank you again @seh for your great work and time!
You are most welcome, @damtur. If we're contemplating a major version bump, this might also be our one chance to fix these longstanding go lint violations:
If you're interested in fixing those—breaking lots of callers in the process, yes—let me know and I'll file a separate PR with those renaming and documentation additions. |
Hey @seh I think keeping migration process as simple as possible with this version bump should be the goal. I don't see enough value in changing those right now. |
Weird: I thought I had responded to this 13 days ago when I also wrote a parallel reply:
Where would you like this documentation? Posted here in this proposal, or in note attached to a GitHub release, or maybe in the top-level README.md file? I was waiting on your response before writing that documentation, and then I thought that maybe I had missed your reply. It turns out that I didn't post my question properly, despite me being sure that I did write it. |
@seh I've merged the code now as it's good. I'll create a release while I get your note. Please write it anywhere so I have it and I will include it in a release note. Thank you for all your work! |
Addressing both #58 and #29, this patch series started with the former and expanded to cover the latter as well. My testing confirms that fargo can communicate with a Eureka server of version era both pre-1.2.2 and post-1.2.2 using both XML and JSON, which is when the change in port value encoding came into service.
The challenges communicating with Eureka on each side of this version boundary involve several fields in the JSON encoding for both instances and the application-level query response:
Pre-Eureka v1.2.2: string
Post-Eureka v1.2.2: number
Pre-Eureka v1.2.2: string
Post-Eureka v1.2.2: number
Pre-Eureka v1.2.2: number
Post-Eureka v1.2.2: string
Furthermore, when registering instances with Eureka, post version 1.2.2 the server started requiring that JSON-encoded instances include the "dataCenterInfo.@Class" field, so we have to synthesize that, while still accommodating custom data center types for which a caller will need to specify a class value.
Finally, I discovered that fargo couldn't decode an instance's data center metadata that contains values that look like numbers; the Eureka server will encode them as JSON numbers rather than strings, requiring us to decode them flexibly.
There are a few disparate solutions combined here, but I figured that they sit well enough together under the umbrella of better compatibility with the server. Again, I believe this patch series resolves the longstanding incompatibility with fargo, Eureka post-version 1.2.2, and JSON encoding. We should be able to both register and query applications and instances now.