Remove some unwraps - #10759
Conversation
| /// Lock a memory reservation, recovering from a poisoned lock. | ||
| /// | ||
| /// A poisoned lock only means that some other thread panicked. The reservation it | ||
| /// guards is plain size accounting, so there is no broken invariant to protect, and | ||
| /// recovering it is always preferable to panicking. | ||
| pub(crate) fn lock_reservation( | ||
| reservation: &Mutex<Option<Box<dyn MemoryReservation>>>, | ||
| ) -> MutexGuard<'_, Option<Box<dyn MemoryReservation>>> { | ||
| reservation.lock().unwrap_or_else(PoisonError::into_inner) | ||
| } |
There was a problem hiding this comment.
Maybe a bit weird. Alternatives to consider:
- Do a single
unwrap()here and just document crate-wide that poisoning causes panics - Use
parking_lots non-poisoning mutexes
| for key in names { | ||
| let (name, value) = self.infos.get_key_value(&key).unwrap(); | ||
| // `infos` is a `BTreeMap`, so it iterates in sorted order already | ||
| for (name, value) in &self.infos { |
Replace `unwrap`/`expect` calls that can never fail with non-panicking equivalents: * `from_usize(0).unwrap()` -> `usize_as(0)` * `NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()` -> `NaiveDate::default()` * `NaiveTime::from_hms_opt(0, 0, 0).unwrap()` -> `NaiveTime::MIN` * `x[0..4].try_into().unwrap()` -> `[x[0], x[1], x[2], x[3]]` * `i256::from(10).checked_pow(64).unwrap()` -> square of a `10^32` literal * `last_mut().unwrap()` -> `if let ... && let Some(last)` * `NullBufferBuilder::materialize_if_needed` now returns the builder * `partial_cmp(..).unwrap()` -> `as_usize().cmp(..)` in `RunEndBuffer` * let-else and `is_some_and` in `regexp_match` and `is_ascii` * `SqlInfoDataBuilder::build` sorts the entries instead of looking them up * `IpcWriteOptions::try_new` matches on the alignment Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A poisoned lock only means that some other thread panicked. The reservation it guards is plain size accounting, so there is no broken invariant to protect, and `MutableBuffer::truncate`, `try_resize`, `clear` and friends have no reason to panic for it. Adds `pool::lock_reservation`, so the recovery is written once instead of at each of the ten lock sites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`infos` is a `BTreeMap`, so it already iterates in key order. Collecting the entries into a `Vec` and sorting them was redundant before this branch too; this drops the copy and the sort instead of rewriting them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ordered_indices` is built from `0..indices_len` and the `indices_len == 0` case already returns above, so the `last()` fallback could never run. Index the last entry directly and say why that is in range. Also spells out the two other invariants the same function and `GenericByteArray::is_ascii` rely on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rewriting `x[..4].try_into().unwrap()` as `[x[0], x[1], x[2], x[3]]` keeps the same panic, it just hides it from `missing_panics_doc`. Bring back the reason each read is in bounds, and use `as_chunks` in the Bloom filter so the word length is a type-level guarantee rather than a comment. `read_footer_length` and `FooterTail::try_new` read from fixed-size arrays, so the compiler already rejects an out-of-bounds index there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`truncate` and `append_n` only touch the last byte when the bit length is not a multiple of eight, and a non-zero remainder already implies at least one byte. The `last_mut()` fallbacks could never run, so they hid the invariant rather than upholding it. Index the last byte and say why it is there. Note that these were never reachable panics either, so `truncate` and `append_n` behave exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`try_schema_from_ipc_buffer` read its length prefix through `try_into().unwrap()`. Read it with `split_first_chunk::<4>()` instead, which makes the length a type-level guarantee and folds in the manual `len < 8` check. `read_record_batch` unwrapped the array it had just decoded, to reuse it for a duplicate projection entry. Hold the decoded array in the `Option` it already had instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
aaeb570 to
b59a5d8
Compare
09c4a52 to
e6d8981
Compare
Rich-T-kid
left a comment
There was a problem hiding this comment.
overall mostly looks good but I left a few comments. Id also like to double check the bit level arithmetic in arrow-buffer/src/builder/boolean.rs
| // `NaiveDate::default()` is documented to be 1970-01-01 | ||
| let epoch = NaiveDate::default(); |
There was a problem hiding this comment.
nit: we can remove this repeated comment since
impl Default for NaiveDate {
fn default() -> Self {
NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()
}
}
| // `NaiveDate::default()` is documented to be 1970-01-01 | |
| let epoch = NaiveDate::default(); | |
| let epoch = NaiveDate::default(); |
There was a problem hiding this comment.
i echo this, especially since we name the variable epoch anyway
| fn materialize_if_needed(&mut self) -> &mut BooleanBufferBuilder { | ||
| let (len, capacity) = (self.len, self.capacity); | ||
| self.bitmap_builder | ||
| .get_or_insert_with(|| Self::materialize(len, capacity)) | ||
| } |
There was a problem hiding this comment.
aren't there performance ramifications for this 🤔 ?
There was a problem hiding this comment.
Yeah - the hot path should be slightly faster now, since the unwraps are gone!
There was a problem hiding this comment.
id be interested to see some numbers around this
| pub(crate) fn lock_reservation( | ||
| reservation: &Mutex<Option<Box<dyn MemoryReservation>>>, | ||
| ) -> MutexGuard<'_, Option<Box<dyn MemoryReservation>>> { | ||
| reservation.lock().unwrap_or_else(PoisonError::into_inner) | ||
| } |
| None => (None, None), | ||
| }; | ||
|
|
||
| if is_flags_scalar.is_some() && is_rhs_scalar != is_flags_scalar.unwrap() { |
There was a problem hiding this comment.
I think there are some clippy lints we can enable to avoid if conditions like this
There was a problem hiding this comment.
@emilk I noticed you made a couple other PR's related to clippy lints I think stricter lints could help out. any thoughts?
There was a problem hiding this comment.
I don't think there is a clippy lint smart enough to find this pattern… except for clippy::unwrap_used (…which is a really good lint imho, but a big task for arrow-rs)
There was a problem hiding this comment.
A quick search for is_some.*\n?\s*\.unwrap find nothing more in the code though
There was a problem hiding this comment.
A quick search for is_some.*\n?/*unwrap find nothing more in the code though
thank you for checking 🙏
| } | ||
| }; | ||
|
|
||
| if regex.is_none() { |
There was a problem hiding this comment.
similar idea here, clippy lints could help avoid code like this
There was a problem hiding this comment.
Again: forbidding all .unwrap()s is the only way to catch all of these.
I am personally a huge proponent of forbidding all .unwrap():s - as this PR shows, they are almost always hiding either a bug, or hiding a smarter and faster way of doing something. But forbidding unwraps should be done on day one of a code base, and doing it now might be too much work… but I might still try in the future :)
| let (Some(start), Some(end)) = (offsets.first(), offsets.last()) else { | ||
| return true; | ||
| }; |
There was a problem hiding this comment.
this is actually a speed up, nice!
There was a problem hiding this comment.
could you elaborate on this speedup? as far as i know this should essentially be dead code since offsetbuffer can never be empty
Jefffrey
left a comment
There was a problem hiding this comment.
im a bit ambivalent about some of the changes here, especially regarding the indexing ones
| pub fn append_null(&mut self) { | ||
| self.offsets_builder.push(self.current_offset); | ||
| self.sizes_builder.push(OffsetSize::from_usize(0).unwrap()); | ||
| self.sizes_builder.push(OffsetSize::usize_as(0)); |
There was a problem hiding this comment.
| self.sizes_builder.push(OffsetSize::usize_as(0)); | |
| self.sizes_builder.push(OffsetSize::zero()); |
could also do this
| let pow_32: i256 = i256::from(10).checked_pow(32).unwrap(); | ||
| // 10^32 fits in an i128, and 10^64 is its square, well below i256::MAX | ||
| let pow_32 = i256::from_i128(10_i128.pow(32)); | ||
| let pow_64 = pow_32.wrapping_mul(pow_32); |
There was a problem hiding this comment.
another option is replacing these with constants
| let (Some(start), Some(end)) = (offsets.first(), offsets.last()) else { | ||
| return true; | ||
| }; |
There was a problem hiding this comment.
could you elaborate on this speedup? as far as i know this should essentially be dead code since offsetbuffer can never be empty
| // `v` is longer than `MAX_INLINE_VIEW_LEN`, so it has at least four bytes | ||
| prefix: u32::from_le_bytes([v[0], v[1], v[2], v[3]]), |
There was a problem hiding this comment.
technically the indexing is still panic possible, we're just hiding the unwrap now 🤔
| // `NaiveDate::default()` is documented to be 1970-01-01 | ||
| let epoch = NaiveDate::default(); |
There was a problem hiding this comment.
i echo this, especially since we name the variable epoch anyway
| // Pad last byte with 1s. | ||
| // A non-zero remainder means there already is a last byte. | ||
| let cur_len_bytes = bit_util::ceil(self.len, 8); | ||
| self.buffer.as_slice_mut()[cur_len_bytes - 1] |= !((1 << cur_remainder) - 1); |
There was a problem hiding this comment.
not sure about these either; it doesnt seem a gain if we're trying to remove unwrap
| let mut block = Block::ZERO; | ||
| for (i, word) in chunk.chunks_exact(4).enumerate() { | ||
| block[i] = u32::from_le_bytes(word.try_into().unwrap()); | ||
| // `as_chunks` gives `[u8; 4]` words, so the conversion cannot fail |
There was a problem hiding this comment.
| // `as_chunks` gives `[u8; 4]` words, so the conversion cannot fail |
comment isnt really needed
| fn materialize_if_needed(&mut self) -> &mut BooleanBufferBuilder { | ||
| let (len, capacity) = (self.len, self.capacity); | ||
| self.bitmap_builder | ||
| .get_or_insert_with(|| Self::materialize(len, capacity)) | ||
| } |
There was a problem hiding this comment.
id be interested to see some numbers around this
Which issue does this PR close?
Rationale for this change
This is one of four PRs splitting up the
clippy::missing_panics_docwork.unwraps #10759 - remove unreachable panics#[expect(clippy::missing_panics_doc)]#10761 -#[expect]the unreachable ones, so the lint can be turned onWhat changes are included in this PR?
Replaces
unwrap/expectcalls that cannot fail with non-panicking equivalents.Are these changes tested?
Covered by the existing tests. Every change here is a rewrite of code whose
panic was unreachable, so there is no new behavior to test.
Are there any user-facing changes?
No.