ref(managed): Add Managed::zip function.#6088
Conversation
| where | ||
| S: Counted, | ||
| { | ||
| let (first, meta) = first.destructure(); |
There was a problem hiding this comment.
Should be able to build this with existing primitives, e.g. map, similar to merge_with.
That would also assert that the Counted impls for (T, S) matches T + S
There was a problem hiding this comment.
I updated this to use map, take a look!
| { | ||
| let (first, meta) = first.destructure(); | ||
| let second = second.accept(|second| second); | ||
| Managed::from_parts((first, second), meta) |
There was a problem hiding this comment.
There is a problem here where the meta of the two managed instances does not match. We should at least add a debug_assert to assert they are matching.
There was a problem hiding this comment.
Because I changed this to use map, I deleted the use of meta. I believe this is now obsolete.
Let me know if you think differently. Thanks!
| /// | ||
| /// The returned instance uses the metadata from `first`. `second` is accepted, transferring | ||
| /// outcome responsibility to the merged instance. | ||
| pub fn merge<S>(first: Self, second: Managed<S>) -> Managed<(T, S)> |
There was a problem hiding this comment.
Should be next to the merge_with method.
| /// | ||
| /// The returned instance uses the metadata from `first`. `second` is accepted, transferring | ||
| /// outcome responsibility to the merged instance. | ||
| pub fn merge<S>(first: Self, second: Managed<S>) -> Managed<(T, S)> |
There was a problem hiding this comment.
I'd argue a function that turns F<A> and F<B> into F<(A,B)> should be called zip. The new function's signature also doesn't match with merge_with.
There was a problem hiding this comment.
zip is a good name, the initial idea of merge was a signature of merge(A, B, f) -> S where f: FnOnce(A, B) -> S but I think zip is a powerful enough as the merge could be implemented as zip().map()
There was a problem hiding this comment.
Okay sounds good. I can rename the function. zip makes sense.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c1e3f13. Configure here.
|
|
||
| let second = second.accept(|second| second); | ||
| (first, second) | ||
| }) |
There was a problem hiding this comment.
Zip skips metadata equality check
Medium Severity
Managed::zip accepts two Managed values with no check that they share the same Meta, yet it routes all later outcomes through first’s metadata after second is accepted. Mismatched scoping or outcome aggregators can mis-attribute or drop quota outcomes for the second value in release builds.
Reviewed by Cursor Bugbot for commit c1e3f13. Configure here.
There was a problem hiding this comment.
Would be great if you can assert that at least the scoping of the Meta matches and document the behaviour that zip will use the meta of the first instance.
Bonus: if you add a test for it
|
Alright I got the main pipelines to pass @Dav1dde and I made the changes. Let me know what you think! Thanks. https://github.com/getsentry/relay/actions/runs/27785392311/job/82223873618?pr=6088#step:7:166 |
Dav1dde
left a comment
There was a problem hiding this comment.
Thanks, looks great!
Documenting the behaviour of what happens to the meta is important to me to make it clear it's not an oversight or "random", the debug_assert is useful because we should never merge two Managed instances from different origins.
|
|
||
| let second = second.accept(|second| second); | ||
| (first, second) | ||
| }) |
There was a problem hiding this comment.
Would be great if you can assert that at least the scoping of the Meta matches and document the behaviour that zip will use the meta of the first instance.
Bonus: if you add a test for it
| first.map(|first, recordkepping| { | ||
| for (category, quantity) in second.quantities() { | ||
| recordkepping.modify_by(category, quantity as isize); |
There was a problem hiding this comment.
| first.map(|first, recordkepping| { | |
| for (category, quantity) in second.quantities() { | |
| recordkepping.modify_by(category, quantity as isize); | |
| first.map(|first, records| { | |
| for (category, quantity) in second.quantities() { | |
| records.modify_by(category, quantity as isize); |
Just me nitpicking to mirror the implementation of merge_with.
There was a problem hiding this comment.
I can do that. It was also a spelling error 😅
|
Changed the title to make the CI check happy |


#5985
Added a generic Managed::merge helper that consumes two managed values and returns a single Managed<(T, S)>,
transferring outcome responsibility into the merged wrapper while preserving metadata from the first value.
Also added a unit test covering the new behavior: the merged value retains both inner values, emits outcomes
for both through the first managed handle when dropped, and prevents the second managed value from emitting
duplicate outcomes after the merge.
I also ran:
Legal Boilerplate
Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms.