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