Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

docs: Update backwards compatibility policy for Rust SDK #32655

Closed
wants to merge 3 commits into from
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 143 additions & 10 deletions docs/src/developing/backwards-compatibility.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Backward Compatibility Policy
title: Backwards Compatibility Policy
---

As the Solana developer ecosystem grows, so does the need for clear expectations around
Expand All @@ -11,16 +11,17 @@ and so this document attempts to clarify and codify the process for new releases
### Expectations

- Solana software releases include APIs, SDKs, and CLI tooling (with a few [exceptions](#exceptions)).
- Solana software releases follow semantic versioning, more details below.
- Software for a `MINOR` version release will be compatible across all software on the
same `MAJOR` version.
- Solana software releases *do not always* follow semantic versioning, more details below.
- Software for a `MINOR` version release will be compatible with the previous 3
`MINOR` releases, and following 3 `MINOR` releases, accounting for skipped minor
versions, more details below.

### Deprecation Process

1. In any `PATCH` or `MINOR` release, a feature, API, endpoint, etc. could be marked as deprecated.
2. According to code upgrade difficulty, some features will be remain deprecated for a few release
2. According to code upgrade difficulty, some features will remain deprecated for a few release
cycles.
3. In a future `MAJOR` release, deprecated features will be removed in an incompatible way.
3. At least 4 `MINOR` releases later, deprecated features may be removed in an incompatible way.

### Release Cadence

Expand All @@ -30,7 +31,7 @@ updates of a particular `MINOR` version release.

#### Release Channels

- `edge` software that contains cutting-edge features with no backward compatibility policy
- `edge` software that contains cutting-edge features with no backwards compatibility policy
- `beta` software that runs on the Solana Testnet cluster
- `stable` software that run on the Solana Mainnet Beta and Devnet clusters

Expand All @@ -40,6 +41,10 @@ updates of a particular `MINOR` version release.
deprecated features. Client SDKs and tooling will begin using new features and endpoints
that were enabled in the previous `MAJOR` version.

The Rust SDK will not have any future `MAJOR` releases, and will stay on `1.X` to
provide a better developer experience. More details in
[Why not just use semver](#why-not-just-use-semver).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this is a bit too restrictive.
There seems to be no reason to say we will never increase the major release number.
It does seem quite unlikely at the moment.
But things may change.

We do want to be explicit about the currently chosen default policy of staying on 1.x.
So we can, maybe, say something along the lines of:

We do not expect MAJOR version to be changed. Any breaking changes will be done though the deprecation process described in [...] in a MINOR version.

If you think it is too subtle, it is probably fine to keep the more assertive "no MAJOR version updates" statement as well.

Also, this paragraph seems to contradict the previous one, that explain when happens in a major version update. If we are saying they never happen, there is no point in this explanation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this is a bit too restrictive.

You're right, I think I was a little too inspired by our chats.

Also, this paragraph seems to contradict the previous one, that explain when happens in a major version update. If we are saying they never happen, there is no point in this explanation.

And you're right about the contradiction -- this document also covers RPC endpoints, and I didn't want to have the policy apply to both RPC and SDK.

I'll make it clearer which parts cover RPC and which parts cover the SDK.


#### Minor Releases (1.x.0)

New features and proposal implementations are added to _new_ `MINOR` version
Expand All @@ -48,6 +53,20 @@ on the testnet, `MINOR` versions are considered to be in the `beta` release chan
those changes have been patched as needed and proven to be reliable, the `MINOR` version will
be upgraded to the `stable` release channel and deployed to the Mainnet Beta cluster.

The Rust SDK may contain breaking changes and removal of previously deprecated features
in a `MINOR` release. According to code upgrade difficulty, every `MINOR` release
ilya-bobyr marked this conversation as resolved.
Show resolved Hide resolved
will be compatible with at least the 3 preceding `MINOR` releases and 3 future
`MINOR` releases.

For example, software using version `1.20` will be compatible with versions up
to `1.23` going forward, and back to `1.17`. Any release numbers that are skipped,
such as `1.15`, are not considered in this calculation.

At a projected cadence of 4 minor releases per year, that gives developers at least
one year to update a project.
ilya-bobyr marked this conversation as resolved.
Show resolved Hide resolved

More details in [Why not just use semver](#why-not-just-use-semver).

#### Patch Releases (1.0.x)

Low risk features, non-breaking changes, and security and bug fixes are shipped as part
Expand Down Expand Up @@ -87,12 +106,13 @@ Patch releases:
Minor releases:

- New APIs

Major releases

- Removal of deprecated APIs
- Backwards incompatible behavior changes
joncinque marked this conversation as resolved.
Show resolved Hide resolved

Major releases:

- Are not projected to happen ever. More details in [Why not just use semver](#why-not-just-use-semver).

### CLI Tools

Patch releases:
Expand Down Expand Up @@ -155,3 +175,116 @@ circumvented in order to rapidly deploy a fix, depending on the severity of the
CLI tooling json output (`output --json`) compatibility will be preserved; however, output directed
for a human reader is subject to change. This includes output as well as potential help, warning, or
error messages.

### Why not just use semver

The Solana Rust SDK crates use a model for introducing breaking changes that does
*not* follow semantic versioning (semver).

#### Semver in Rust

Under semver, breaking changes, such as removal of functions or types, only happens
in a new major version.

In many situations, this is useful -- it's very difficult to pick up a breaking
change accidentally. The default dependency declaration in Cargo follows this
concept. It assumes that all minor versions are compatible, and will automatically
update to the newest minor version available.

For example, if you declare `solana-program = "1.10"`, Cargo can pull in version
`1.16`, since it assumes that all `1.X.Y` releases are compatible.

Additionally, Cargo will force all crates in a build to use the same major version
of a dependency. For example, if your crate `my-crate` declares `solana-program = "1.10"`,
and a dependency `my-dependency` declares `solana-program = "1.16"`, both your crate and your
dependency will be built with version `1.16`, at least.
ilya-bobyr marked this conversation as resolved.
Show resolved Hide resolved

Additionally, the types in `solana-program` used by both your crate and your
dependency are treated as the same.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not true.
There is nothing special about direct and indirect dependencies.
Each crate just lists a set of dependencies, that may have dependencies of their own.
And the same compatibility rules are used to determine if a single instance of a crate can be used to satisfy several requirements, or if multiple versions are necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove this whole part as you suggested previously, since there's no need for us to describe cargo


For example, if `solana-program` has a struct declared as:

```
#[derive(PartialEq)]
pub struct Pubkey(pub [u8; 32]);
```

And `my-dependency` has:

```
pub const DEPENDENCY_PUBKEY = Pubkey([1; 32]);
```

Then `my-crate` can say:

```
use solana_program::Pubkey;
let my_pubkey = Pubkey([1; 32]);
if my_pubkey == my_dependency::DEPENDENCY_PUBKEY {
println!("They're the same");
}
```

On the other hand, types from crates with different major versions are treated as
completely different types. If `my-crate` declares `solana-program = "2"` and
`my-dependency` declares `solana-program = "1"`, then the preceding code *will not*
compile.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, programs are not used as dependencies :P
Maybe rephrase, using solana-sdk as the example crate?
This way it would be closer to the practical problem at hand.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

solana-sdk can't be used on-chain, so on-chain programs must use the types from the solana-program crate, and all of the problems from the upgrade stem from the solana-program crate.

So if we want to be practical, solana-program is the better example. But if you find it very unclear, we can change to solana-sdk.


To get this code to compile, you need to create functions to convert between the
types in v1 and v2. And whenever v3 is released, the entire problem becomes multiplied.

#### So what's your solution?

Rather than forcing developers to disentangle these kinds of messes on every
major version release, our solution is to keep everything on the same major version.

However, to avoid chaos and ensure that no downstream client ever breaks from updating
from one `MINOR` version to the next, we must provide new functions or structs
and deprecate the old ones.
ilya-bobyr marked this conversation as resolved.
Show resolved Hide resolved

We have broken this commitment in the past, but through expanded downstream
testing and conscientiously exposing APIs, we aim to never repeat that mistake.
If it does happen accidentally, we will do whatever is necessary to restore
the functionality, within reason.
ilya-bobyr marked this conversation as resolved.
Show resolved Hide resolved

To avoid maintaining too much old code, however, we reserve the right to remove
deprecated code after at least one year, or roughly 4 `MINOR` versions later.

This model has precedents in the Rust ecosystem. Through
[Rust editions](https://blog.rust-lang.org/2021/05/11/edition-2021.html#what-is-an-edition),
the Rust developers can introduce backwards-incompatible changes, all while staying
on major version 1. As crate developers, we have less flexibility than a language,
but the concept is similar.
ilya-bobyr marked this conversation as resolved.
Show resolved Hide resolved

Additionally, the
[Minimum Supported Rust Version (MSRV) Policies](https://github.com/rust-lang/api-guidelines/discussions/231)
suggest that updating MSRV is *not* a semver breaking change, even though it
effectively forces users to upgrade their compiler just to update a crate.

By ensuring that projects will never immediately break, and giving developers
one year to update, this model will keep some stability while avoiding stagnation.
joncinque marked this conversation as resolved.
Show resolved Hide resolved

#### But why was the upgrade to 1.16 so difficult?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can understand how this is close to your heart, but do you really think this discussion belongs to the policy doc? :P

We have issues with all the details, and going forward it would not matter any more, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah you're absolutely right 😅 I wanted to provide some historical record so we know what motivated this change. What do you think about adding it as a post-mortem in #32503 ?


In version `1.16` of the Rust crates, the Borsh dependency was upgraded from
`0.9` to `0.10`, which constituted a breaking change.

Downstream developers faced unsolvable resolution problems because their dependencies
were also forced to use `1.16`, but if the dependencies were not explicitly updated
to use Borsh `0.10`, they were met with incompatible traits, since they implemented
`BorshSerialize` and `BorshDerialize` on version 0.9, which as described earlier,
joncinque marked this conversation as resolved.
Show resolved Hide resolved
are treated as completely different traits from `BorshSerialize` and `BorshDeserialize`
on version 0.10.

To address this problem, we've implemented the older Borsh 0.9 traits on required
joncinque marked this conversation as resolved.
Show resolved Hide resolved
types, and exposed the Solana Borsh utilities through explicit versioning. This
allows us to upgrade to any other version of Borsh without ever breaking a
downstream developer.

In a year, however, we reserve the right to remove the implementations of the
Borsh 0.9 traits and potentially break downstream projects that have not updated
joncinque marked this conversation as resolved.
Show resolved Hide resolved
within the last year.

Even if the Rust crates implemented proper semantic versioning, downstream
projects would have been broken as soon as they tried to mix `Pubkey`s from
a non-updated dependency with `Pubkey`s from the new major version.