-
Notifications
You must be signed in to change notification settings - Fork 223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] Managing breaking changes to existing traits #100
Comments
I like the safety and caution of this approach, but it feel like it's more suited to post-1.0 quality software. What would the version numbers look like in the above process, semver-minor or semver-patch? I noticed you didn't define any timeframes for these releases. To keep things flowing, I think the deprecation process should be pretty quick at the stage the ecosystem is at now. Personally, I'd prefer to have this stuff break catastrphically (as per the Alternative section) and fix it once, but I can see that causing a lot of pain for people with more embedded Rust written than myself. |
I agree in principle, but I think if we want to get it right by 1.x we should start practicing it now and work out what works and doesn't. In addition, I suspect
Roughly following the semver spec for breaking changes, so for 0.x: patch, patch, minor, and for >1.x: minor, minor, major. The first stage is not breaking and should be updated automatically to notify consumers, the second only requires a feature flag change to opt out, the third explicitly breaks anything not updated. I can see an argument that the second two stages should both be considered breaking, the flags will have to be updated to work, and I'm not completely convinced either way. It's also important to note that there is likely to be than one change applied per release.
Agreed that it should be quick. I'd like to see us iterating and releasing more regularly, and I think having some process like this would decrease the risk / scariness of releasing so we can do it faster. I think if we had a minimum limit of a couple of weeks between each stage we could have a similar release cadence. This approach also lets us collect feedback on and learn from our releases / changes and the impact they have on our consumers, and to stop part way if there are issues, where breaking changes are otherwise not communicated except for the breakage. |
I'm generally in favor. The one concern I have is what consequences this has on CI. If I understand correctly, all combinations of features will have to be tested. Could it cause problems to have that many CI builds (i.e. running up against usage limits)?
I disagree with this part. Upgrading to a new patch (0.x) or minor version (>= 1.x) should never break the build (except to fix security issues). Even if the fix to the breaking change is as simple as adding a feature. |
@ryankurte Your primary plan sounds good to me. |
I hope breaking migrations are not so frequent as to require many more builds.
Fair enough, do you have a preferred arrangement, or would you be happy with 0.x: patch, minor, minor, and >=1.x: minor, major, major? While it deviates from strict semver for 0.x, it benefits us if the first deprecation marking is an automatic upgrade. |
I'd be very happy with that. I don't see how it deviates from semver. Adding a deprecation won't break the build, unless the crate denies warnings, which is effectively opting out of semver stability. |
This sounds very good @ryankurte.
|
I just noticed @therealprof and @ryankurte having a discussion on IRC that they really should be having here, in my opinion :) @therealprof said this:
This brings up an issue that is hugely important in my opinion: You can't have multiple drivers among your dependencies that depend on different versions of the trait. If one driver enables the features, than all drivers that don't enable the feature are suddenly broken and won't build. Unless I miss something, there's no other way to fix the broken build than fork one group of drivers and change them to do what the other group of drivers is doing. I think this makes all other advantages that this proposal has moot. What's the point of having a smooth transition, if everything breaks down the moment I use two drivers that are at different stages of the smooth transition? In light of this, I now prefer the following approach:
Yes, that's one additional breakage, but at least it won't break my build if I mix two drivers. That whole thought about mixing and matching drivers, has led me to another insight: It's possible to import different versions of the same package under different names: rust-lang/cargo#4953 That means, a HAL implementation can release a transition version that supports both and old and a new version, allowing application authors to use any driver during the transition period. |
After thinking about it some more and some discussion on IRC I have to revise my previous statement. I see two fundamental problems with a part of this approach (the
I'd really want to avoid a
|
Good points ^_^ I kindof figured breaking changes should be few and far between, and our goal would be to keep most of our environment in sync with the hal, but the rand crate experience seems not great.
I like that we'd be giving notice to consumers of depreciation, I don't like that we'd effectively be causing more breakage and thus fragmentation of dependencies.
It seems to me that if there's a way we can do the work on our side it'd be better than expecting it of the whole ecosystem? I just had a go at supporting both by including a previous hal version and writing adaptors (as suggested by @therealprof I think, but I may have misunderstood the suggestion), but, it seems like it would become a bit of a nightmare to manage if we ever had overlapping breakages / dependency requirements.
How would you approach this? I don't love the idea of version numbers in the api.
Pretty onboard with this, we have a common understanding with semver of what a version number change means, it's simple for us and easy to understand, and it only breaks once. |
If that's feasible, then we should do it. As an author/co-maintainer of several HAL implementations, I'm glad to have that in my toolbox though.
So we're back to the simple solution? I'm not opposed to this, but I think for a foundational crate like I see your points regarding version numbers in traits and the additional breakage, but I think both problems are not that important:
Another point to consider: Still, I'd be fine with clean, breaking releases, if that's the majority consensus. But I do think it's not the best solution, and not even a good solution in a post-1.0 world. Suggestion: |
I definitely agree that we need to be more cautious for 1.0, and as mentioned above I think we should try to have whatever approach we decide on in place and tested before we get there. +1 for a clean break for #97 so we can land it and move forward ^_^ |
Relevant discussion in #92: |
Hey, thanks for linking that back here (I'll try do that for any future discussions too), and I appreciate everyone's efforts in working to get this right ^_^ All my responses here are in the context of the alternative proposal here: #100 (comment) @therealprof from #92 (comment)
I appreciate the sentiment, I'm worried about fragmenting the ecosystem too, though I agree with @hannobraun here that having separate traits for fallible and infallible pins and leaving users to handle the interop issues long term would be more of a problem. We're also at 0.x, I don't think the possibility of breakage is outside of anyones expectations, I think the impact of adding an error type is pretty minimal, and I'd like to hope that this is a very rare event. Whatever approach we take, we can monitor and collect feedback to improve for (the hopefully not required) next time.
Afaik the same trait in different crate version is incompatible, so it seems to me that having a larger set of versions and traits over which we apply the changes is going to cause the same headache as my proposal with combinations of versions across dependencies instead of features.
It sucks to have that impact, though should we go with the non-breaking merge and rename approach it would then require you to fix everything twice, and break all your downstreams twice? It seems to me that is also likely to leave people behind. Unfortunately I can't see the unpublished crates to collect data on them, but I know I'll have a bunch to fix too, and maybe there's an opportunity for us to rally the community around updating the published crates (while it's still october anyway). @hannobraun your post about multiple crate versions got me thinking, I think (in this instance at least) it should be possible to backport adaptors for breaking traits to the previous series? If we were to release 0.3 as a breaking change, then 0.2.x could include 0.3 with I like the idea of this in that it doesn't block us from moving forward, shouldn't put any future requirements on new releases or require continued support in master, and is hopefully reasonably easy to explain and use. I dislike the precedent it sets for maintaining historic branches and the resulting opportunity for 0.2.x to hang around (which is part of the reason for my openness to a hard breaking change). |
I strongly disagree. Not only should we avoid unnecessary breakage at any cost (we had far too much necessary breaking changes in the last month and there's only so much breakage users will tolerate without moving on), also the severity of the previously planned breakage (replacing the infallible traits with fallible ones under the same name, that is) is far from minimal, rather the opposite: It will instantly break all existing applications and drivers as soon as one component pulls in the new version.
Yes, that's exactly the reason why we should not reuse the same traits.
Yeah, not to happy about that either. I'd prefer to simply give the new traits fresh names and let the old names fade away into irrelevance and then remove them. We have a nice
Unless we decide to yank the old version (and instabreak all current uses of it immediately; probably the easiest way to create a giant shitstorm in the Rust universe!) it is going to hang around anyway. Before moving to a new minor (or major!) version reasonable folks will always consider:
Using new traits under the same name would make the dependent crate immediately incompatible with all other crates using the old version so crate authors are likely to hold off with the changes until a considerable part of the whole ecosystem decided to make a move, which could potentially take a long time or even not happen at all. That's a huge risk and quite frankly -- given the rare cases of beneficial use of the fallible trait version -- not one I'd want to take lightly. |
Wouldn't this problem be solved by employing the semver trick?
I really don't see this danger of leaving people behind.
Any crate that's "left behind" under these circumstances is essentially abandoned, and unless you're going to maintain all of those crates, that's nothing we can prevent :)
I understand where you're coming from but I'd really hate if in 5 years, I have to type Yes, let's be super careful and super conservative about making these changes. Yes, let's keep those deprecated traits around until they have faded into irrelevance. But please, let's not lock us into cumbersome trait names forever, due to some stupid mistakes we've made in what's basically the stone age of the embedded Rust ecosystem. Whether we call it [1] Although I prefer |
Well, my 2 cents here:
I for one couldn't any less whether we call it |
So I wrote a long reply without a good conclusion and decided to try implementing the semver trick inspired no-breakage update I mentioned earlier, I ended up applying the breaking change and backporting an implementation of the new traits into an 0.2.x branch to enable compatibility between them, rather than using
[updated with new links] Which I think gives us:
I couldn't work out how to adapt the |
I gave v0.2.3 a quick spin and for some reason it breaks I2C for me:
|
I also breaks I2C in other crates with other drivers, e.g. SSD1306. |
Huh, how strange! I based my 0.2.x branch on master, which does have plenty of changes, but I can't see any that should break I2C. I just released |
Yes, same problem. I'm using my
|
Right, it seems the fault occurs as soon as you add embedded-hal |
Oh gosh, you are totally right, my bad. I am not used to caret being inferred / not explicit.
Seems like a good working theory tbh.
Fair concern, seems fine from my side though and a fresh clone works for me (on
|
As I've said before: If one is using semantic versioning correctly then just incrementing the patch version shouldn't have any impact at all; those are compatible versions and will be treated as such by cargo. Fiddling with the traits makes the version incompatible, as can be observed by running
You might be totally right about the I2C problem vanishing into thin air by using a released crate instead of pointing to a git (or in my case local) repository, so I'm not worried about that at all. If it still breaks after a release we'll have to address this properly... I am still wary and worried about the trait reuse for GPIO. The |
Sorry, that was my bad. Whatever the problem was, it's gone after
But that's just it, what @ryankurte did really shouldn't have any impact! I can't see any reason why it wouldn't be a correct use of semver. The It looks rather like
I'm not ruling out the possibility that I'm still missing something big, but at this point, it seems pretty clear to me that your objections are based on misunderstandings. It seems to me that:
I still have some reservations about the nature of the blanket impl's, and would like to see more extensive testing. But I don't see any big conceptual problems with the approach at this point. |
Yeah this was totally a misunderstanding of cargo semver on my part sorry, there have definitely been some on this side too 😅
If we base things on the previous breaking change we should replace any identical traits with the v03 ones, then maybe we don't need to re-export the whole v03 hal, just the new bits? idk. Also thanks for clearing up all my misunderstandings here ^_^ |
No it wasn't. Maybe
I would boldly claim that my picture is very clear. This proposal (reusing the trait names) is in the black magic realm and that kind of magic tends to fail in mysterious and sometimes spectacular ways which is exactly why I don't like it. |
Yes, we absolutely need to replace identical traits with re-exports, otherwise the whole semver trick won't work. How we re-export additions to the API is an implementation detail, I think, and not really important in the grand scheme of things. I don't have a preference right now (although a single
No problem, always glad if I can help :)
I am baffled by this comment. First, making breaking changes to APIs without changing the names of those APIs is a practice that is absolutely pervasive and likely as old as the concept of APIs. The only novel thing here is the proposed approach to alleviating the breakage (which I wouldn't call reusing the name; bridging between the new and old versions seems more precise). Second, you saying your picture is clear, then calling the proposal "black magic" seems to be a contradiction. Isn't "black magic" an expression used for things that are not understood or understandable? The only coherent interpretation of this comment that I can come up with is that you don't fully understand the proposal, don't think it can be fully understood, and therefore it's very clear to you we shouldn't use it. I don't think that this is a very useful stance and I don't think this proposal is black magic. I think it is an interesting approach that can and will be understood. That understanding might unearth reasons why it can't work in practice, therefore my call for further testing. |
No, reasonable projects don't do that unless there's an important reason to do it (which there isn't IMHO). The way this is typically done is to introduce a new API and deprecate the old API, then phase it out.
It is clear to me, that doesn't mean it is clear to everyone. That's exactly the problem with black magic, the wizards-in-charge think it's a great idea at the time until someone or something changes their mind... If you don't like the term black magic, we can also call it a footgun. Also (as expressed several times) I'm concerned about the users, not the few participants of this discussion.
And I disagree and don't appreciate your misguided interpretations of my comments. |
Misc notes as an idle watcher of this thread:
Also, again as an outsider here (and as someone who has admittedly not read all of the context), this discussion seems to be getting a little heated. We're all working (hard, and in free time), to get this right. Perhaps it might be better to take this to a lower latency format to discuss iteratively? e.g. IRC, Hangouts, call, etc? edit: clarified cargo's version resolution behavior. |
I'm not disagreeing with that. I was observing that the practice is pervasive, not that it is good or should be emulated. This was brought on by my misunderstanding of your comment.
I'm very sorry for misunderstanding your comment. I didn't mean to offend you. That still leaves me very confused about what your misgivings are specifically. In any case, I won't attempt to continue this part of the discussion for now. I think it's obvious that one of us is operating based on misunderstandings, and I'd like do my homework to make sure that it isn't me. (Please, don't take this previous sentence as any kind of attack. I'm very aware of the fact this it could be me who's wrong, and all I'm saying is I want to experiment more before I'm ready to continue here).
Thank you, I'll keep that in mind.
I agree that we should try our best to not let this discussion get (more) heated, and I'd be happy to have a call with anyone who's willing to have one. I think that real-time communication is very valuable for the social function it has (in so far as it can remind us that we're all in the same boat here). But I doubt that any form of low-latency communication will be useful for resolving the technical argument. |
No offence taken.
I'm opposing the reuse of the same trait names for the fallible versions because I think there's nothing inherently wrong with the infallible versions that would warrant such a radical API change because there's a good chance that this will cause confusion and frustration down the road. My preferred approach is to name the fallible traits differently, mark the old ones as deprecated, keep the fallible->infallible |
@therealprof Thank you for that summary. It's actually very helpful in understanding where you're coming from, as I got lost in the details of the discussion, and this brings us back to the big picture. Let me try to summarize my own big-picture position in a similar way. While I agree that it's important to minimize breakage, I'm concerned that in doing so, we end up with baggage that we'll be saddled with forever (for example in the form of names that are less understandable and straight-forward than they could be). I believe that it's likely that the embedded Rust ecosystem will grow significantly in the future. However minor the effect of these less-than-optimal names may be in isolation, they will add up over a large number of users over many years, leading to a significant amount of confusion. Therefore I think it's worth putting effort into finding an approach that can potentially allow us to end up with a simpler end result, without causing pain right now. I'm not certain what my preferred approach is, but I think @ryankurte's proposal looks promising. I think our disagreement comes mainly from the fact that we're weighing things differently. I put a lot of importance on the simplicity of the end result, and think some complexity during the transition is warranted to achieve that. I think you put more importance on keeping the process simple, and avoiding approaches that could potentially backfire. Do you think this is a fair assessment? I've realized two things that I think should inform the discussion from here on:
Given that, I'd like to propose the following. Short-term (i.e. right now), we do this:
Longer-term, we talk about what the values and priorities should be for the development of this project, as mentioned above. We probably won't come to a consensus here, but I think it's fine to come up with a few drafts and decide on one using majority vote. Thoughts? |
Sounds good. |
I agree, I wonder whether it'd be useful to talk about goals/objectives/expectations for the embedded-hal somewhere else, though that discussion should hopefully not block us making changes, maybe a big issue that people can drop their perspectives into could be a way to start?
I agree also. My perspective is that the hal exists to provide simple compatibility, and anything that can result in fragmentation of implementations is worth being wary of. There are a lot of people using the project now, hopefully in the future it will be orders of magnitude more, but it does depend on us getting this right.
Sounds good to me. |
I agree that a such an issue sounds like a good way to start. I think it would also be a good idea if someone wrote up the consensus that seems to have developed here (release breaking changes under new names, research other approaches, revisit later) and opened a pull request that adds this as guidelines to this repository. Then we can have a proper team vote on it. |
Hey all, I raised in #108 that I think we need to provide adaptors between the new/old traits so we don't end up with fragmentation / incompatible implementations (though under a separate PR would be fine), any disagreement with that? Assuming for now we're in agreement, @hannobraun you raised concerns about |
107: Update changelog for v0.2.2 release and tick version r=ryankurte a=thenewwazoo Added updates to changelog (worded them as best I could based on reading PRs and code). AFAICT there are no breaking changes here (e.g. those being discussed in #100) Co-authored-by: Brandon Matthews <bmatthews@zipcar.com> Co-authored-by: Ryan <ryankurte@users.noreply.github.com>
Hey, sorry for the delay. I'm away from the office right now, and this got buried under loads of unhandled notifications.
Assuming the adapters work, I can't think of a reason why anyone would need to implement both trait versions. I just wanted to bring it up, in case someone else can think of a reason why this would be a problem.
I don't have other ideas. If I were to go ahead with this personally (which I don't have time for right now), I'd want to experiment with Since I'm not going to do the work, and don't have concrete concerns, I won't object to going ahead with this. |
108: Add fallible version of the digital traits and deprecate the current ones r=ryankurte a=eldruin There seems to be an agreement in [#100](#100 (comment)) on how to proceed with the fallible traits. This adds the fallible traits under `digital::v2` and marks the current ones as deprecated. Co-authored-by: Diego Barrios Romero <eldruin@gmail.com> Co-authored-by: Ryan <ryankurte@users.noreply.github.com>
108: Add fallible version of the digital traits and deprecate the current ones r=ryankurte a=eldruin There seems to be an agreement in [#100](#100 (comment)) on how to proceed with the fallible traits. This adds the fallible traits under `digital::v2` and marks the current ones as deprecated. Co-authored-by: Diego Barrios Romero <eldruin@gmail.com> Co-authored-by: Ryan <ryankurte@users.noreply.github.com>
127: Digital v1 <-> v2 compatibility wrappers and shims r=japaric a=ryankurte This PR introduces implicit v1 -> v2 (forward) compatibility, and explicit wrapper types for v2 -> v1 (reverse) compatibility between digital trait versions as a final step for #95, as well as moving the deprecated v1 traits to a re-exported module for clarity. As @japaric pointed out, it is not feasible to have implicit compatibility in both directions, so it seemed reasonable to make regression explicit as it hides an `.unwrap()` on failure. @therealprof, @hannobraun, @eldruin what do you think of this approach? I think it probably needs more documentation, though I am definitely open to suggestions as to what / where. See also: #100, #92, #102. Co-authored-by: Ryan Kurte <ryankurte@gmail.com> Co-authored-by: Diego Barrios Romero <eldruin@gmail.com> Co-authored-by: Daniel Egger <daniel@eggers-club.de>
more discussion of how cursed breaking updates are in #135 :-( |
Following the hal team meeting and updated process I think we can close this. Thank you all for your attention and effort! |
As highlighted in #95, #97 and #99, there are likely to be situations in which we need to fix or upgrade traits in breaking ways, and these are likely to have significant ramifications on the embedded ecosystem. This RFC proposes an approach to manage these breaking changes.
Goals:
Approach:
We propose a staged approach for managing breaking changes, with some time between each stage to allow API consumers to update.
In the first release, replacement traits are introduced and feature-gated allowing users to opt in to their use. Feature names start with with
use-
and a description of the change, for exampleuse-fallible-digital-traits
for the new fallible digital trait update in #97. Existing traits are marked#[deprecated]
to alert users to the need to update, and this opt-in flag should be added to the CI build matrix. Documentation should also be updated to recommend the use of the opt-in traits and the related github issue for the change. This stage alerts HAL users of deprecation and allows immediate migration should they desire, without requiring any explicit action.In the second release, the opt-in feature flag is replaced with an opt-out flag to allow regression to the previous trait if required, this should start with
regress-
. Continuing the fallible digital trait example this would beregress-infallible-digital-traits
. This opt-out flag should be added to the build matrix, and the previous flag removed. Documentation should be updated to mention opt-out trait availability if required. This stage will break HAL consumers using the previous traits, requiring explicit action to update or defer the change.In the third release, the old traits are removed, feature flags are removed from the package and the CI build matrix, and the documentation should be updated to remove mention of the opt-out trait. This stage will break any hal consumers that have not updated to the new traits.
In each release, these changes will be documented in the changelog (as is standard) and should be published through other useful means. The linking of the related issue in the documentation should allow for feedback from HAL consumers at each stage.
Each stage will correspond to a semver version increase (though it is worth noting that more than one breaking change may be included in a single release). At
0.x
this will bepatch, minor, minor
, and>=1.x
: will beminor, major, major
.I also wrote a small tool to analyse the dependency requirements, resolutions, and features of published crates (https://github.com/ryankurte/rust-dep-check), which would let us track migration through versions and feature flags. An example output showing the current use of
embedded-hal
:Questions
unproven
? My view is that given the propensity ofunproven
traits we may as well try to mitigate all breakages or no worry about it at all (see the alternative).Alternative
Do nothing, semver already mitigates breaking changes and we’re in the 0.x series with no API stability guarantees. This is much simpler, though not particularly supportive of our API consumers or the possible impact of breaking changes on the ecosystem, and it doesn't give our consumers any notice of the change outside of the github discussions.
cc. @hannobraun @therealprof
The text was updated successfully, but these errors were encountered: