-
Notifications
You must be signed in to change notification settings - Fork 302
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
Add Clone
for Request/Response/Extensions
#574
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatives:
- Wrap the
dyn Any
in anArc
instead ofBox
, forgo theExtensions::get_mut
method. We could provide aExtensions::swap
instead. Perhaps it's possible to emulateget_mut
by doing something akin toArc::make_mut
. - The
Extensions::clone()
returnsExtensions::new()
.
Neither of them are compelling.
@@ -31,7 +30,7 @@ impl Hasher for IdHasher { | |||
/// | |||
/// `Extensions` can be used by `Request` and `Response` to store | |||
/// extra data derived from the underlying protocol. | |||
#[derive(Default)] | |||
#[derive(Default, Clone)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The derive(Clone)
here means that the NotCloneExtension
will remain in the clone* AnyMap
, as NotCloneExtension(None)
. Is this what we want?
An alternative here might be:
- The
HashMap
store values asenum Value { Clone(Box<dyn CloneAny>), NotClone(Box<dyn Any>) }
, Clone
filters out theValue::NotClone
,- Have two methods
fn insert
andfn insert_clone
, only the latter requiringClone
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The derive(Clone) here means that the NotCloneExtension will remain in the new AnyMap, as NotCloneExtension(None). Is this what we want?
Im not totally following what you mean by this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let mut extensions_a = Extensions::new();
extensions_a.insert(NotCloneExtension::new(3_u32));
let extensions_b = extensions_a.clone();
let value = extensions_b.get::<NotCloneExtension<u32>>();
value
here is Some(NotCloneExtension(None))
from what I read from the implementation. Is this what we want? If we take the changes bulleted above, then we could filter out the non-Clone
extensions.
Having two separate methods fn insert
and fn insert_clone
then storing them as Value::NotClone
and Value::Clone
respectively, has the additional benefit of:
- It's an addition rather than a breaking change.
- It doesn't require a newtype when using
get
- e.g.get::<T>
will retrieveT
when it's inserted viainsert
orinsert_clone
. This is in contrast toget::<NotCloneExtension<T>>
andget::<T>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right but now get returns an enum rather than option , I guess we could have the enum be three variants though not sure if that is better or not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like this is what I was imagining
It doesn't compile yet, but if we used some more of the anymap
tricks I guess it would?
I think it makes more sense to let users add the Arc themselves rather than force it upon them? What if they want to use a Rc?
This just feels misleading and doesn't support an extension existing through the clones. |
Is separating (or unbundling, if you prefer) |
@LukeMathWalker I don't think that changes much since we still need a single type to pass through. One option, would be to change tower to accept something like |
That's the direction I would have envisioned by unbundling. |
I think that making extensions cloneable is a good thing, but I think I agree with @LukeMathWalker. However, I don't think changing Tower to accept Before moving on, I wanted to add some notes on my interest in this feature:
In any case, I don't feel too strongly about the existence of extensions inside of |
Connection level/before streaming body failures are generally a thing you want to retry.
The plan is to implement good defaults in the batteries included retry middleware to prevent this stuff. I think no matter what we want to support retries and let downstream consumers decide if they want to enable it or not.
I think the ergonomics of it existing with one of the most used types in the ecosystem has a huge benefit. I think generally speaking we've been pretty happy with extensions and I have not seen many users complain about it. I think this extensions style has been successfully adopted by the aws sdk as well. I don't think doing something like I think another thing we need to think about here is timelines and what makes sense. I don't know if anyone has the time to explore changing up context in tower and the deadline for a http 1.0 is fast approaching. I think we may want to consider keeping things how they are and making a change like this just improves some flexibility while not losing out on the benefits of the previous implementation. |
In the AWS SDK, the
We use a similar property bag pattern in the SDK, but we ended up wrapping |
Regarding moving the extensions out to a separate type...it's worth noting that much of the value from having a notion of "extensions" comes from it being defined in the For example, Because the extensions are part of the AFAICT, a model where other libraries provide the context/extension type wrapping a |
(i'm little sick, so I apologize if this response isn't particularly coherent or well-edited.)
Fair enough!
I had a longer response, but after re-reading this, I'm probably over-indexing on this point and was probably steeped too long in the context of Thrift-based RPC, so I replaced it with this sentence.
Sure, I can see that.
🤷. I think I've had stronger opinions in the past but they're not so strong now. I think most of my complaints boil down to aesthetics and vibes, which isn't really the best basis for making decisions like these.
I haven't really been keeping pace with Hyper's roadmap to 1.0, so I'm assuming it's trying to hit 1.0 this year, which: fair.
I keep forgetting about Hyper's usage of extensions in upgrades; I think that usecase is sufficiently compelling that it overrides any aesthetic concerns I might have.
That is a compelling motivation and I think the reason I forgot about this is that I haven't been steeped in writing http-based services (or any services, for that matter) for nearly the last two years. In any case, I trust y'all will make the right decision and this PR is an improvement to the status quo. |
To clarify: I was not suggesting to remove It would open up a few possibilities for crates that depend on |
Was wondering if there was any movement on this? Making Request Clone would solve some of our problems for use of the http Request and Response types |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@LucioFranco consensus in the issue was to move forward with this. Would you be able to update the PR?
/// A newtype that enables non clonable items to be added | ||
/// to the extension map. | ||
#[derive(Debug)] | ||
pub struct NotCloneExtension<T>(Option<T>); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to include this convenience type within http
itself.
@@ -28,6 +28,8 @@ bytes = "1" | |||
fnv = "1.0.5" | |||
itoa = "1" | |||
|
|||
anymap = "1.0.0-beta.2" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we provide the functionality without the extra dependency?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that was my initial goal but to poc I just used it and as you can see its patched so we probably want to vend anyways.
I am irrationally excited this might become get merged. @LucioFranco let me know if I can help |
This was done in #395 |
Proposal
Allow
Request<T>
/Respnose<T>
to implementClone if T: Clone
. The current blocker for this is thatExtensions
doesn't implementClone
. This is due to the complicated nature ofAny
andClone
. Though it is a solvable problem shown bydyn-clone
and theanymap
(beta releases) crates.The solution in this PR swaps out our own
AnyMap
implementation to use theanymap
crate1.0.0-beta.2
version that supports clonable values. The downside to this change is that it nows requires all the types that get inserted into the anymap to implement clone which is not a backwards compatible change and will break end users. To support this use case this PR adds aNotCloneExtension
(pending name, open to ideas) that allows users to implement Clone for a type T that doesn't implement Clone. This is done by storing the inner type as an Option and when cloning the newtype it will set the value to None. This allows users to for example, have different extension instances per requests in the retry middleware.The benefit of
Request<T>
/Response<T>
implementing clone will greatly improve the ergonomics for end users and simplify things like the tower retry layer.Closes #395
cc @seanmonstar @hawkw @davidbarsky @hlbarber @LukeMathWalker
Notes
This PR uses a forked version of the anymap crate that adds a specialized
extend_map
fn. The plan I think going forward would be to vendor the minimal set of code from the anymap crate and to not depend on it for 1.0 of http.