7

We're currently using thrift for developing our micro-services. When I recently came across this below issue.

Assume below is the thrift contract for Summary Object and there is an API which gets and updates summary using the summary object passed.

Version - 1.0

struct Summary {
    1: required string summaryId,
    2: required i32 summaryCost
}

Summary getSummary(1: string summaryId);

void updateSummary(1: Summary summary);

Now let's say there are 5 services which are using this 1.0 contract of Summary.
In the next release we add another Object called a list of summaryvalues.

So the new contract would look like

Version - 2.0

struct Summary {
    1: required string summaryId,
    2: required i32 summaryCost,
    3: optional list<i32> summaryValues
}

Summary getSummary(1: string summaryId);

void updateSummary(1: Summary summary);
  1. So when this below list is populated, we save the list of values summaryValues aganist that summaryId.
  2. And when the client sends this list as null we remove the existing values saved for that 'summaryId`.

Now the problem occurs when other services which are using OLDER version of the thrift contract (Version 1.0) try to call getSummary and updateSummary.
The intention of the Older Client by calling updateSummary was to set another value for summaryCost. However since this client doesn't contain the object summaryValues it sends the Summary object with summaryValues as null to Server.

This is resulting in the Server removing all existing values of summaryValues for that summaryId.

Is there a way to handle this in thrift? The isSet() Methods don't work here, as they try to perform a simple null check.
Every time we release a newer client with modification to existing objects, we're having to forcefully upgrade client versions of other servers even though the change is not related to them.

Kishore Bandi
  • 5,537
  • 2
  • 31
  • 52
  • FWIW the core question here: "If a service storing an object `{a: 42, b: 99, c: 7}` receives a call to update it to `{a: 0, b: 0}` from a client that doesn't know about the `c` field, what value should `c` be assigned?" isn't specific to Thrift. – Gordon Gustafson Oct 12 '18 at 18:12

1 Answers1

4

In your version 2.0 the contract of the updateSummary() method has changed (ie, now it allows to save and remove summary values):

Option 1: Instead of changing its behaviour, create a new method (ie, updateSummaryV2()) and start using it across the latest version of your clients, while deprecating the old one.

This way, older versions of the client still use the normal updateSummary() without conflicting with new method's contract and assumptions.


Option 2: Add an optional field that contains the api version and has a default value set to the latest API version:

struct Summary {
    1: required string summaryId,
    2: required i32 summaryCost,
    3: optional list<i32> summaryValues
    4: optional i32 apiVersion = 2
}

This way, if apiVersion is not set, you know the request came from an old client, and for future versions, you will known the client version and can react accordingly.


Alternatively, you could only remove the records if an empty list is provided and do nothing if the list is not set, to respect the previous method contract.

As a side note: having an action depend on something implicit (here, the absence of a list) can be risky in general, even without considering the cross-compatibility issue. It's generally safer (and easier to work with and maintain) when such actions depend on an explicit flag.

Shastick
  • 1,218
  • 1
  • 12
  • 29
  • Creating another API would make maintenance of the code very cumbersome because we need to remember what feature was added as part of which API. Things could easily go out of hand. Also, Having an emptylist might not be possible, as we have to inform all dependent clients to add this behavior even though they're not interested in this field. – Kishore Bandi Dec 13 '16 at 16:28
  • "we need to remember what feature was added as part of which API" -> That sounds like something you likely have to do anyway in a multi-version API context :). I've updated the answer with another option. – Shastick Dec 14 '16 at 09:52
  • @BandiKishore -> any feedback on the second option? :) – Shastick Dec 16 '16 at 09:28
  • +1 for the different suggestions, but was looking for a cleaner solution :) This was actually one of the solutions we were thinking of using and instead of relying on developer updating the value of this version every time a feature goes, we thought of using the version of the client jar itself (populated in properties file during build using `${project.version}`) and using this to identify the version that client is using. *Splitting the comment in 2 due to limitation* – Kishore Bandi Dec 16 '16 at 20:34
  • We didn't go ahead with that solution as we thought it wasn't clean and seemed more of a hack. Further we thought a better approach was to have separate flags (for each feature) which client would set when they want to clear the value. For smaller features this would be preferred but for bigger ones which have nested objects being modified, we still don't have a cleaner solution. – Kishore Bandi Dec 16 '16 at 20:37