• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Edition Lifetimes
2
3Now that Edition Zero is complete, we need to re-evaluate what the lifetimes of
4features and editions look like going forward.
5
6## Background
7
8The implementation of editions today was based largely on
9[Protobuf Editions Design: Features](protobuf-editions-design-features.md) and
10[Life of an Edition](life-of-an-edition.md) (among other less-relevant docs).
11Specifically, the latter one takes a strong stance on the lifetimes of both
12editions and features. Many of the ideas around editions have since been
13simplified in [Edition Naming](edition-naming.md), where we opted for a stricter
14naming scheme owned and defined by us. In the process of rolling out editions to
15various protoc plugins and planning for edition 2024, it's become clear that we
16may need to re-evaluate the feature lifetimes as well.
17
18*Editions: Life of a Feature* (not available externally) is an alternate vision
19to *Life of an Edition*, which tries to put tighter constraints on how features
20and editions interact. It also predicted many of the problems we face now, and
21proposes a possible solution.
22
23## Overview
24
25Today, features and editions are largely disconnected from each other. We have a
26set of features that govern various behaviors, and they can all be used in any
27edition. Each edition is simply a distinct set of defaults for **every**
28feature. Users can override the value of any released feature in any edition.
29Each generator and protoc itself all advertise a range of editions they support,
30and will reject any protos encountered outside that range.
31
32This system does have the nice consequence that behavior-preserving editions
33upgrades can always be performed (with respect to the most current proto
34language). It separates editions from breaking changes, and means that we only
35need to worry about one versioning scheme (our OSS release).
36
37While this all works fine in Edition 2023 where we only have a single edition,
38it poses a number of problems going forward. There are three relevant events in
39the lifetime of both editions and features, introduction, deprecation, and
40removal. Deprecation is essentially just a soft removal to give users adequate
41warning, and the same problems apply to both.
42
43### Introducing an Edition
44
45Because every generator plugin (and protoc) advertises its edition support
46window, introducing a new Edition is well-handled today. We get to enjoy all the
47same benefits we saw rolling out Edition 2023 in every subsequent edition (e.g.
48we can make radical language changes gated on edition).
49
50### Dropping an Edition
51
52Dropping support for an edition doesn't really mean that much today. We *could*
53do it simply by bumping up the minimum supported edition of a binary in a
54breaking release. However, that would have no relation to our feature support,
55and at best would allow us to clean up some parser code branching on editions.
56This code would always be tied to language changes we made in the introduction
57of a new edition, where we could finalize them with the removal of an edition.
58
59### Introducing a Feature
60
61Whenever we introduce a new feature, we need to make sure to specify its
62defaults for **every** edition (protoc enforces that every edition has a known
63set of defaults). It will also immediately be overridable in every edition. This
64means that pre-existing binaries that have declared support for an old edition
65may suddenly be presented with protos that override a feature they can't
66possibly know about.
67
68We faced this problem when introducing the new `string_type` feature as part of
69*New String APIs* (not available externally). Our solution at the time was to
70create some ad hoc validation that prohibited overriding this feature in Edition
712023 until we were ready to release it. This solution doesn't work in general
72though, where in OSS we can have arbitrarily old binaries floating around (and
73even in google3 we can easily have up to six-month-old binaries within the build
74horizon). These old binaries wouldn't have that validation layer, and would
75happily process Edition 2023 files with `string_type` overrides, despite not
76knowing how to properly treat that feature.
77
78### Dropping a Feature
79
80On the other end of the spectrum, we need a way to deprecate and then remove
81support for features. Given that we expect most features to remain for many
82years, we haven't been forced to consider this situation too much. The current
83plan of record though, is that we would do this by first marking the feature
84field definition `deprecated`, and then remove it entirely in a breaking release
85(and total burndown in google3).
86
87The problem with this plan is that it creates a lot of complexity for users
88trying to understand our support guarantees. They'll need to track the lifetime
89of **every** feature they use, and also difficult-to-predict interactions
90between different versions of protoc and its plugins. If we drop a global
91feature in protoc, some plugins may still expect to see that feature and become
92broken, while others may not care and still work.
93
94## Recommendation
95
96### Feature Lifetimes
97
98We recommend adding four new field options to be used in feature specifications:
99`edition_introduced`, `edition_deprecated`, `deprecation_warning`, and
100`edition_removed`. This will allow every feature to specify the edition it was
101introduced in, the edition it became deprecated in, when we expect to remove it
102(deprecation warnings), and the edition it actually becomes removed in.
103
104We will also add a new special edition `EDITION_LEGACY`, to act as a placeholder
105for "infinite past". For editions earlier than `edition_introduced`, the default
106assigned to `EDITION_LEGACY` will be assigned and should always signal the *noop
107behavior that predated the feature*. Proto files will not be allowed to override
108this feature without upgrading to a newer edition. Deprecated features can get
109special treatment beyond the regular `deprecated` option, and a custom warning
110signaling that they should be migrated off of. For editions later than
111`edition_removed`, the last edition default will continue to stay in place, but
112overrides will be disallowed in proto files.
113
114For example, a hypothetical feature might look like:
115
116```
117optional FeatureType do_something = 2 [
118    retention = RETENTION_RUNTIME,
119    targets = TARGET_TYPE_FIELD,
120    targets = TARGET_TYPE_FILE,
121    feature_support {
122      edition_introduced = EDITION_2023,
123      edition_deprecated = EDITION_2025,
124      deprecation_warning = "Feature do_something will be removed in edition 2027",
125      edition_removed = EDITION_2027,
126    }
127    edition_defaults = { edition: EDITION_LEGACY, value: "LEGACY" }
128    edition_defaults = { edition: EDITION_2023, value: "INTERMEDIATE" }
129    edition_defaults = { edition: EDITION_2024, value: "FUTURE" }
130];
131```
132
133Before edition 2023, this feature would always get a default of `LEGACY`, and
134proto files would be prohibited from overriding it. In edition 2023, the default
135would change to `INTERMEDIATE` and users could override it to the old default or
136the future behavior. In edition 2024 the default would change again to `FUTURE`,
137and in edition 2025 any overrides of that would start emitting warnings. In
138edition 2027 we would prohibit overriding this feature, and the behavior would
139always be `FUTURE`.
140
141### Edition Lifetimes
142
143By tying feature lifetimes to specific editions, it gives editions a lot more
144meaning. We will still limit this to breaking releases, but it means that
145**all** of the editions-related breaking changes come from this process. When we
146drop an edition, breaking changes will always come from the removal of
147previously deprecated features. By regularly dropping support for editions, we
148will be able to gradually clean up our codebase.
149
150#### Edition Upgrades
151
152A consequence of this design is that edition upgrades could now become
153potentially breaking. Any proto files using deprecated features could be broken
154by bumping its edition to one where the feature has been removed. Within
155google3, we would need to completely burn down all deprecated uses before we can
156remove the feature.
157
158This is not a substantial change on our end from the existing situation though,
159where we'd still need to remove all uses before removing it. The key difference
160is that we have the *option* to allowlist some people to stay on an older
161edition while still moving the rest of google3 forward. We would also be able to
162continue testing removed features by allow-listing dedicated tests to stay on
163old editions.
164
165#### Garbage Collection
166
167Another consequence of this is that we can't actually clean up feature-related
168code until every edition before its `edition_removed` declaration has been
169dropped. This ties feature support directly to edition support, especially in
170OSS where we can't forcibly upgrade protos to the latest edition.
171
172#### Predictability
173
174The main win with this strategy is that it clarifies our guarantees and makes
175our library more predictable. We can guarantee that a proto file at a specific
176edition will not see any behavioral changes unless we:
177
1781.  Make a breaking change outside the editions framework.
1792.  Drop the edition the proto file uses.
180
181We can also guarantee that as long as users stay away from deprecated features,
182they will still be able to upgrade to the next edition without any changes.
183
184### Implementation
185
186Fortunately, this design would be **very** easy to implement right now. We
187simply need to add the new field options and the new placeholder edition, and
188then implement new validation in protoc. Because the two error conditions (using
189a feature outside its existence window) and the warning (using a deprecated
190feature) only trigger on *overridden* features, protoc already has all the
191information it needs. Generator feature extensions must be imported to be
192overridden, so the problem of protoc not knowing feature defaults doesn't come
193into play at all.
194
195If we wait until edition 2024 has been released, the situation would be a bit
196more difficult to unravel. Any new features added in 2024 would be usable from
1972023, so we'd have to either intentionally backport support or remove all of
198those uses before enabling the validation layer. Therefore, the recommendation
199is to implement this ASAP, before we start rolling out 2024.
200
201#### Runtimes with Dynamic Messages
202
203None of the generators where editions have already been rolled out require any
204changes. We likely will want to add validation layers to runtimes that support
205dynamic messages though, to make sure there are no invalid descriptors floating
206around. Since they all have access to protoc's compiled defaults IR, we can pack
207as much information in there as possible to minimize duplication. Specifically,
208we will add two new `FeatureSet` fields to `FeatureSetEditionDefault` in
209addition to the existing `features` field.
210
211*   overridable_features - The default values that users **are** allowed to
212    override in a given edition
213*   fixed_features - The default values that users **are not** allowed to
214    override in a given edition
215
216We will keep the existing `features` field as a migration tool, to avoid
217breaking plugins and runtimes that already use it to calculate defaults. We can
218strip it from OSS prior to the 27.0 release though, and remove it once everyone
219has been migrated.
220
221In order to calculate the full defaults of any edition, each language will
222simply need to merge the two `FeatureSet` objects. The advantage to splitting
223them means that we can fairly easily implement validation checks in every
224language that needs it for dynamic messages. The algorithm is as follows, for
225some incoming unresolved `FeatureSet` user_features:
226
2271.  Strip all unknown fields from user_features
2282.  Strip all extensions from user_features that the runtime doesn't handle
2293.  merged_features := user_features.Merge(overridable_defaults)
2304.  assert merged_features == overridable_defaults
231
232This will work as long as every feature is a scalar value (making merge a simple
233override). We already ban oneof and repeated features, and we plan to ban
234message features before the OSS release.
235
236Note, that there is a slight gap here in that we perform no validation for
237features owned by *other* languages. Dynamic messages in language A will naively
238be allowed to specify whatever language B features they want. This isn't
239optimal, but it is in line with our current situation where validation of
240dynamic messages is substantially more permissive than descriptors processed by
241protoc.
242
243On the other hand, owners of language A will have the *option* of easily adding
244validation for language B's features, without having to reimplement the
245reflective inspection of imports that protoc does. This can be done by simply
246adding those features to the compilation of the defaults IR, and then not
247stripping those extensions during validation. This will have the effect of tying
248the edition support window of A to that of B though, and A won't be able to
249extend its maximum edition until B does (at least for dynamic messages). For
250generators in a monorepo like Protobuf's this seems fine, but may not be
251desirable elsewhere.
252
253### Patching Old Editions
254
255In [Edition Naming](edition-naming.md) we decided to drop the idea of "patch"
256editions, because editions were always forward and backward compatible. We would
257only ever need multiple editions in a year if somehow we managed to speed up the
258rollout process and wanted faster turnaround. This changes those assumptions
259though, since now editions are neither forward-compatible (new features don't
260work in old editions) or backward-compatible (old features may not work in new
261editions).
262
263Hypothetically, if there were a bug in the editions layer itself we may require
264a "patch" edition to safely roll out a fix. For example, imagine we discover
265that our calculation of edition defaults is broken in edition 2023 and we had
266accidentally released it. If we've already fixed the issue and released edition
2672024 as well, we can't just create a `2023A` "patch" to fix the issue because
268editions are represented as integers (and 2023 and 2024 are adjacent). We would
269want to release some kind of fix for people still on edition 2023 though, so
270that they can minimally upgrade before 2024 (which may be a breaking edition).
271
272What we could do in this situation (if it ever arises) is introduce a new
273integer field in `FileDescriptorProto` called `edition_patch`. It would take
274some work to fit this into feature resolution and roll it out to every plugin,
275but given that we've hidden the edition from most users
276([Editions Feature Visibility](editions-feature-visibility.md)) it shouldn't be
277too bad. As long as patches never introduce or remove features or change their
278defaults, protoc and plugins can always use the latest patch they know about to
279represent that edition.
280
281### Documentation
282
283As part of this change, we need to document all of this publicly for
284plugin/runtime owners. We should create a new topic in
285https://protobuf.dev/editions/ to cover all of this, along with other relevant
286details they'd need to know.
287
288## Alternatives
289
290### Continue as usual
291
292The only real alternative here is to make no change, which has all of the
293problems listed in the overview of this topic.
294
295#### Pros
296
297*   Requires no effort short-term
298*   Editions upgrades will **never** be breaking changes
299
300#### Cons
301
302*   Likely to cause problems as soon as edition 2024
303*   Introducing new features is dangerous and unpredictable
304*   Dropping features affects all editions simultaneously
305*   The features supported in each edition can vary between protobuf releases
306*   High cognitive overhead for our users. They'd need to track the progress of
307    every feature individually across releases.
308
309### Full Validation for Dynamic Messages
310
311None of the generators where editions have already been rolled out require any
312changes. We will need to add validation layers to runtimes that support dynamic
313messages though, to make sure there are no invalid descriptors floating around.
314Any runtime that supports dynamic messages should have reflection, and the same
315reflection-based algorithm will need to be duplicated everywhere. For each
316`FeatureSet` specified on a descriptor:
317
318```
319absl::Status Validate(Edition edition, Message& features) {
320  std::vector<const FieldDescriptor*> fields;
321  features.GetReflection()->ListFields(features, &fields);
322  for (const FieldDescriptor* field : fields) {
323    // Recurse into message extension.
324    if (field->is_extension() &&
325        field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
326      CollectLifetimeResults(
327          edition, message.GetReflection()->GetMessage(message, field),
328          results);
329      continue;
330    }
331
332    // Skip fields that don't have feature support specified.
333    if (!field->options().has_feature_support()) continue;
334
335    // Check lifetime constrains
336    const FieldOptions::FeatureSupport& support =
337        field->options().feature_support();
338    if (edition < support.edition_introduced()) {
339      return absl::FailedPrecondition(absl::StrCat(
340          "Feature ", field->full_name(), " wasn't introduced until edition ",
341          support.edition_introduced()));
342    }
343    if (support.has_edition_removed() && edition >= support.edition_removed()) {
344      return absl::FailedPrecondition(absl::StrCat(
345          "Feature ", field->full_name(), " has been removed in edition ",
346          support.edition_removed()));
347    } else if (support.has_edition_deprecated() &&
348               edition >= support.edition_deprecated()) {
349      ABSL_LOG(WARNING) << absl::StrCat(
350          "Feature ", field->full_name(), " has been deprecated in edition ",
351          support.edition_deprecated(), ": ", support.deprecation_warning());
352    }
353  }
354}
355```
356
357#### Pros
358
359*   Prevents any feature lifetime violations for any language, in any language
360*   Easier to understand
361*   Less error-prone
362*   Easy to test with fake features
363
364#### Cons
365
366*   Only works post-build, which requires a huge amount of code in every
367    language to walk the descriptor tree applying these checks
368*   Performance concerns, especially in upb
369*   Duplicates protoc validation, even though most languages perform
370    significantly looser checks on dynamic messages
371
372#### Pros
373
374*   Minimizes the amount of reflection needed
375
376#### Cons
377
378*   Can't validate extensions for languages we don't know about, since they're
379    not built into the binary
380*   Potential version skew between pool and runtime features
381*   Requires reflection stripping unexpected fields
382*   Difficult to understand the algorithm from the code
383