File: evolve_spec.rst

package info (click to toggle)
python-stone 3.3.8-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,956 kB
  • sloc: python: 21,786; objc: 498; sh: 29; makefile: 11
file content (179 lines) | stat: -rw-r--r-- 6,841 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
***************
Evolving a Spec
***************

APIs are constantly evolving. In designing Stone, we sought to codify what
changes are backwards incompatible, and added facilities to make maintaining
compatibility easier.

Background
==========

The root of the problem is that when an API interface evolves, it does not
evolve simultaneously for all communicating parties. This happens for a couple
reasons:

1. The owner of the API does not have control over 3rd parties that have
   integrated their software at some point in the evolution of the
   interface. These integrations may never be updated making
   compatibility-awareness critical.

2. Even the owner of the API may roll out evolutions to their fleet of
   servers in stages, meaning that clusters of servers will have different
   understandings of the interface for windows of time.

Sender-Recipient
================

When discussing interface compatibility, it's best to think in terms of a
message sender and a message receiver, either of which may have a newer
interface.

If the sender has a newer version, we want to make sure that the recipient
still understands the message it receives, and ignores the parts that it
doesn't.

If the recipient has a newer version, we want to make sure that it knows what
to do when the sender's message is missing data.

Backwards Incompatible Changes
------------------------------

* Removing a struct field

  * An old receiver may have application-layer dependencies on the field,
    which will cease to exist.

* Changing the type of a struct field.

  * An old receiver may have application-layer dependencies on the field
    type. In statically typed languages deserialization will fail.

* Adding a new tag to a closed union.

  * We expect receivers to exhaustively handle all tags. If a new tag is
    returned, the receiver's handler code will be insufficient.

* Changing the type of a tag with a non-Void type.

  * Similar to the above, if a tag changes, the old receiver's
    handler code will break.

* Changing any of the types of a route description to an incompatible one.

  * When changing an arg, result, or error data type for a route, you
    should think about it as applying a series of operations to convert
    the old data type to the new one.

  * The change in data type is backwards incompatible if any operation
    is backwards incompatible.

Backwards Compatible Changes
----------------------------

* Adding a new route.

* Changing the name of a stuct, union, or alias.

* Adding a field to a struct that is optional or has a default.

  * If the receiver is newer, it will either set the field to the
    default, or mark the field as unset, which is acceptable since the
    field is optional.

  * If the sender is newer, it will send the new field. The receiver will
    simply ignore the field that it does not understand.

* Change the type of a tag from Void to anything else.

  * The older receiver will ignore information associated with the new
    data type and continue to present a tag with no value to the
    application.

* Adding a new tag to an open union.

  * The older receiver will not understand the incoming tag, and will
    simply set the union to its catch-all tag. The application-layer will
    handle this new tag through the same code path that handles the
    catch-all tag.

Planning for Backwards Compatibility
====================================
* When defining a union that you're likely to add tags to in the future,
  use an open union. By default, unions are open.
  Stone exposes a virtual tag called "other" of void type to generators
  that is known as the "catch-all" tag for this purpose.
  If a recipient receives a tag that it isn't aware of,
  it will default the union to the "other" tag.


Leader-Clients
==============

We focused on senders and recipients because they illustrate the general case
where any two parties may have different versions of a spec. However, your
system may have an added layer of predictability where some party ("leader") is
guaranteed to have the same or newer version of the spec than its "clients."

It's important to note that a leader-clients relationship can be transient and
opportunistic--it's important to decide if this relationship exists in your
setup.

The leader-client relationship comes up often:

1. A service that has an API is the "leader" for first-party or third-party
   clients in the wild that are accessing the service's data. The server
   will get a spec update, and clients will have to update their code to
   take advantage of the new spec.

2. Within a fleet of servers, you may have two clusters that communicate
   with each other, one of which receives scheduled updates before the
   other.

A known leader can be stricter with what it receives from clients:

* When the leader is acting as a recipient, it should reject any struct
  fields it is unaware of. It knows that the unknown fields are not because
  the client, acting as a sender, has a newer version of the spec.

  * Since a client acting as a recipient may have an older spec, it
    should retain the behavior of ignoring unknown fields.

* If the leader is acting as a recipient, it should reject all unknown
  tags even if the union specifies a catch-all.

* If the leader is acting as a recipient, any tag with type Void should
  have no associated value in the serialized message since it's not
  possible for a client to have converted the data type to something else.

[TODO] There are more nuanced backwards compatible changes such as: A tag
can be removed if the union is only sent from the server to a client. Will this
level of detail just lead to errors in practice?

Route Versioning
================

Building language facilities to ease route versioning has yet to be fully
addressed. Right now, if you know you are making a backwards incompatible
change, we suggest the following verbose approach:

* Create a new route.

  * The Stone language syntax supports specifying a version number for a
    route. You can attach the version number to the end of the route name
    separated by a `:`. For example, to introduce version 2 for
    ``/get_account``, use the annotation ``/get_account:2``.

* Copy the definition of any data types that are changing in a backwards
  incompatible way. For example, if the response data type is undergoing an
  incompatible change, duplicate the response data type, give it a new
  name, and make the necessary modifications.

* Be sure to update the route signature to reference the new data type.

Future Work
===========

Building in a lint checker into the ``stone`` command-line interface that
warns if a spec change is backwards incompatible based on the revision history.
This assumes that the spec file is in a version-tracking system like git or hg.