Add Vec type, remove Buffer, add SendResult_drop#41
Conversation
| }); | ||
| } | ||
| let name = &chunk[..first_colon_position]; | ||
| let value = &chunk[(first_colon_position + 1)..]; |
There was a problem hiding this comment.
What if : is the last char from chunk ?
There was a problem hiding this comment.
It's handled as a consequence of other bits of code I think, but I'll add explicit tests; I meant to do that and forgot, so thanks!
There was a problem hiding this comment.
My bad! Indeed in that case value slice will be empty, which is fine, sorry for the noise
ivoanjo
left a comment
There was a problem hiding this comment.
Left a few notes but overall pretty happy!
| #[export_name = "ddprof_ffi_Vec_tag_new"] | ||
| pub extern "C" fn vec_tag_new<'a>() -> crate::Vec<Tag<'a>> { | ||
| crate::Vec::default() | ||
| } | ||
|
|
||
| /// Pushes the tag into the vec. | ||
| #[export_name = "ddprof_ffi_Vec_tag_push"] | ||
| pub unsafe extern "C" fn vec_tag_push<'a>(vec: &mut crate::Vec<Tag<'a>>, tag: Tag<'a>) { | ||
| vec.push(tag) | ||
| } |
There was a problem hiding this comment.
I'm assuming these can fail to allocate memory. What happens in that case -- does a panic get triggered?
There was a problem hiding this comment.
Yes, they will panic. I previously used vec.try_reserve to detect this and return a Result instead, but that requires Rust 1.57 and we are only on 1.56 (and are stuck there for a bit due to Alpine Linux).
There was a problem hiding this comment.
👍 I think we can live with this until we move to a newer version.
| let name = &chunk[..first_colon_position]; | ||
| let value = &chunk[(first_colon_position + 1)..]; | ||
| Ok(Tag { | ||
| name: CharSlice::from(name), | ||
| value: CharSlice::from(value), | ||
| }) |
There was a problem hiding this comment.
Minor: I was looking at the Ruby implementation and it additionally strips any leading/trailing whitespace on each chunk. May be worth including here as well?
There was a problem hiding this comment.
Does Ruby also handle , and as separators, or does it require just one of them? Edit: you already answered this, just ,.
There was a problem hiding this comment.
No, it only supports ,. I hadn't seen the before... 🤔
| return Err(ParseTagsError { | ||
| message: "Tag cannot start with a colon.", | ||
| }); |
There was a problem hiding this comment.
Minor: I think it would be a nice UX if we could include the actual tag that failed to parse in the error message (applies to other error messages added in this PR -- it would be nice to always have at least an excerpt of the actual thing that triggered the failure).
Otherwise a user needs to go dig what's up, and may get confused on which tags made it where, depending on how complex their setup is, and giving them something they can grep their codebase with here I think may save valuable time.
| impl Debug for ParseTagsError { | ||
| fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||
| write!(f, "Tag Error: {}", self.message) | ||
| } | ||
| } | ||
|
|
||
| impl Display for ParseTagsError { | ||
| fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||
| write!(f, "The tags string was malformed.") | ||
| } | ||
| } | ||
|
|
||
| impl Error for ParseTagsError {} |
There was a problem hiding this comment.
My rust-foo is somewhat failing me here. Does this arrangement mean that by default we don't present a more specific error message, and only debug builds get it? If so, why? 😉
There was a problem hiding this comment.
The Rust convention is that Debug is for developers and Display is meant for everyone else. They are both present, and it depends on whether the programmer stringifies it with {} or {:?}.
| /// Parse a string of tags typically provided by environment variables | ||
| /// The tags are expected to be either space or comma separated: | ||
| /// "key1:value1,key2:value2" | ||
| /// "key1:value1 key2:value2" |
There was a problem hiding this comment.
This is interesting; looking at the Ruby code, it really only allows commas. Can you point me in the direction where you found the space-separated version? I may need to bring this up with the rest of the Ruby folks if we're doing this wrong.
There was a problem hiding this comment.
Python previously just supported , and later added space separated to match the agent. Note that as far as I can tell, neither spaces nor commas are supported in tags: https://docs.datadoghq.com/getting_started/tagging/.
However, there is a flaw in this technique for choosing between them. Given tag env:staging:east, the key is env and the value is staging:east but neither method will split this correctly because it assumes only 1 colon is present per key-value chunk. Python has this same flaw.
There was a problem hiding this comment.
One additional thing the page you've linked to is
A tag can be in the format value or :. For optimal functionality, Datadog recommends constructing tags in the : format.
which I'm pretty sure will not work in Ruby either. I'll raise these, thanks!
Update: Slack link to dd-trace-rb discussion.
| #[allow(clippy::ptr_arg)] | ||
| #[export_name = "ddprof_ffi_Vec_tag_clone"] | ||
| pub extern "C" fn vec_tag_clone<'a>(vec: &'a crate::Vec<Tag<'a>>) -> VecTagResult { | ||
| let mut clone = Vec::new(); | ||
| for tag in vec.into_iter() { | ||
| clone.push(tag.to_owned()) | ||
| } | ||
| VecTagResult::Ok(crate::Vec::from(clone)) | ||
| } |
There was a problem hiding this comment.
I'm somewhat curious about this one -- what's the use case for cloning the vec?
There was a problem hiding this comment.
In PHP I store the result of parsing DD_TAGS globally. On each request, I clone these tags and add potentially specific values like runtime-id to the clone, which might help to support fork (it's not complete for PHP though, so maybe misguided).
There was a problem hiding this comment.
Hmmm... have you considered doing that by parsing DD_TAGS globally, adding them once at exporter creation, and then starting from an empty vec when adding the extra tags during send?
(Although I don't mind if you prefer doing it this way, seems harmless as well)
| /// Buffer holds the raw parts of a Rust Vec; it should only be created from | ||
| /// Rust, never from C. | ||
| #[repr(C)] | ||
| pub struct Buffer { |
There was a problem hiding this comment.
RIP Buffer, you served us well but it's time to go 🙇
2e39741 to
76aad26
Compare
21947c5 to
08b4fe5
Compare
The Vec type is a generalization of Buffer. In particular, this PR exposes an API to create and manage Vec<Tag> and replaces existing uses of Buffer with Vec<u8>. It includes a helper to parse DD_TAGS as well. One of the APIs, ddprof_ffi_Buffer_reset, was removed. Previously this was required when using the failure case of the SendResult type, but now there is ddprof_ffi_SendResult_drop, which should be used instead.
Previously it couldn't handle a tags like: env:staging:east value Tag messages on failure have improved. As part of adding tests, added more trait support to Slice and CharSlice so they can be used in equivalence statements. As part of doing that, I realized Slice could implement IntoIterator and that it cleaned up 3+ places where we call .into_slice().iter(). I also removed unsafe from `into_slice`. Technically, it could still fail such as if the user creates a slice of length 4 but only has 2 elements, but this is a user error that cannot really be checked.
| return Err(Box::new(TagsError { | ||
| message: "tag name must not be empty".to_string(), | ||
| })); |
There was a problem hiding this comment.
Minor: Perhaps include here the tag value?
| #[allow(clippy::ptr_arg)] | ||
| #[export_name = "ddprof_ffi_Vec_tag_as_slice"] | ||
| pub extern "C" fn vec_tag_as_slice<'a>(vec: &'a crate::Vec<Tag<'a>>) -> Slice<'a, Tag<'a>> { |
There was a problem hiding this comment.
Minor: Less important than the others, but I guess still makes sense to have as #[must_use]...?
What does this PR do?
Adds:
repr(C)Vectype.Vec<Tag>, including a helper for parsingDD_TAGS.ddprof_ffi_SendResult_dropdestructor.Removes:
Vec<u8>instead.ddprof_ffi_Buffer_reset. Probably you should useddprof_ffi_SendResult_dropinstead, as resetting the buffer was onlyreally used in the failure case of a SendResult.
ddprof_ffi_Buffer_free. There aren't any APIs left which create aBox<Vec<u8>>to use it on.Motivation
I wanted an API in PHP for dynamic tag management, and Ivo said he'd
like it for Ruby as well.
Additional Notes
I started off using opaque types, but cbindgen was failing to generate
correct C code for
Box<Vec<Tag>>when used in an enum. So, instead Imade the FFI vec type. This technically saves an allocation as well.
How to test the change?
Make the above changes, then use as normal. The functions below may be
helpful creating a dynamic array of tags: