-
Notifications
You must be signed in to change notification settings - Fork 74
feat: allocator-api and shared memory support #164
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
base: master
Are you sure you want to change the base?
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.
Pull Request Overview
This PR adds allocator-api integration and shared memory support for NGINX modules by introducing new synchronization primitives, allocator-backed string and data structures, and wrappers around NGINX’s slab and pool APIs.
- Implement shared‐memory reader/writer locks (
sync.rs
) - Add allocator‐aware
NgxString
,RbTree
,SlabPool
, andPool
types - Integrate
allocator-api2
, update Cargo features, and addngx_sched_yield
shim
Reviewed Changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.
Show a summary per file
File | Description |
---|---|
src/sync.rs | RawSpinlock and RwLock for interprocess sync |
src/lib.rs | Reexport allocator and sync modules |
src/core/string.rs | NgxString with alloc support and tests |
src/core/rbtree.rs | RbTree wrapper using nginx red‐black tree API |
src/core/slab.rs | SlabPool allocator over ngx_slab_pool_t |
src/core/pool.rs | Pool wrapper over ngx_pool_t with Allocator |
src/core/mod.rs | Expose new rbtree and slab modules |
src/allocator.rs | TryCloneIn trait and allocator helper functions |
nginx-sys/src/lib.rs | Add ngx_sched_yield fallback implementation |
Comments suppressed due to low confidence (3)
src/core/string.rs:580
- [nitpick] There is no test for the error path of
append_within_capacity
. Adding a case where the buffer is full would ensure correct overflow behavior is covered.
fn test_string_comparisons() {
src/allocator.rs:10
- [nitpick] The
TryCloneIn
trait lacks doc comments on its purpose and usage. Consider adding a brief overview and example in the trait-level doc.
pub trait TryCloneIn: Sized {
nginx-sys/src/lib.rs:181
- The call to
usleep
isn’t imported or qualified, causing a compile error. Consider callinglibc::usleep
or importingusleep
from the proper crate.
usleep(1)
c01f18a
to
a73f48b
Compare
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.
Hi! @pchickey asked me to look at the Allocator
trait bits because I have some experience implementing and using that trait.
The dangling_aligned
helper could probably be simplified a little bit if it didn't have a type parameter, and always returned a NonNull<u8>
. The only caller I saw (unless I missed something) uses T = u8
and so if the type parameter was removed, then that also removes some of the questions around T
's alignment versus the alignment dynamically passed in as an argument, makes some debug_assert!
s unnecessary, etc.
The comments about overriding the provided default Allocator
methods are really opportunistic and are things that would be nice if the underlying capability is there. (And if the underlying capability isn't there right now, you might want to consider whether it is worth adding in the future. See the comment about building up Vec
s.)
But yeah, overall, looks good to me!
(Leaving this review as a "comment" because I don't want to presume myself to be an nginx expert who can authoritatively approve or request changes to PRs or anything like that; just sharing my thoughts after a once-over. Hopefully they are helpful!)
src/core/pool.rs
Outdated
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) { | ||
// ngx_pfree is noop for small allocations unless NGX_DEBUG_PCALLOC is set. | ||
// | ||
// XXX: there should be no cleanup handlers for the allocations made using this API. | ||
// Violating that could result in the following issues: | ||
// - use-after-free on large allocation | ||
// - multiple cleanup handlers attached to a dangling ptr (these are not unique) | ||
if layout.size() > 0 // 0 is dangling ptr | ||
&& (layout.size() > self.as_ref().max || layout.align() > NGX_ALIGNMENT) | ||
{ | ||
ngx_pfree(self.0.as_ptr(), ptr.as_ptr().cast()); | ||
} | ||
} |
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.
Does ngx_pool_t
do non-noop free
s if done in LIFO order? If so, then it would also make sense for it to support in-place realloc
s for the last allocation, and override the provided default methods for Allocator::grow
and Allocator::shrink
here as well.
Allowing the last allocation to grow in-place, even in bump allocators, is particularly nice when building Vec
s of an unknown number of elements and which end up resizing and growing their underlying storage.
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.
Unfortunately, ngx_pool_t
is not capable of that. It does not even track the necessary information.
The pool internals are exposed though, so there's a possibility for implementing this optimization in future. In a fragile and dangerous way :(
pub struct Pool(NonNull<ngx_pool_t>); | ||
|
||
unsafe impl Allocator for Pool { | ||
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> { |
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.
It seems like ngx_pcalloc
exists, so it probably makes sense to implement Allocator::allocate_zeroed
as well.
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.
ngx_pcalloc
is a combination of ngx_palloc
and memset
, the same as the default implementation of Allocator::allocate_zeroed
. A correct implementation of Allocator::allocate_zeroed
for ngx_pool_t
would inherit all of the Allocator::allocate
complexity, swap ngx_palloc
with ngx_pcalloc
and zeroize the data in the remaining branches.
Besides the obvious maintenance cost increase, I assumed that the default Allocator::allocate_zeroed
has more potential for inlining and optimizations.
/// Wrapper for a locked [`ngx_slab_pool_t`] pointer. | ||
pub struct LockedSlabPool(NonNull<ngx_slab_pool_t>); | ||
|
||
unsafe impl Allocator for LockedSlabPool { |
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.
Similarly, it may make sense to provide non-default implementations of Allocator::{allocate_zeroed,grow,shrink}
here as well, if the underlying ngx_slab_pool_t
supports those operations (even if only for the last allocation).
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.
Again, not something ngx_slab_pool_t
is capable of doing.
It's possible to make grow
noop if it fits into the same slot, but that requires reproducing a part of the slab pool internals in Rust.
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.
Some minor issues I found up to "feat: owned byte string type with Allocator support"
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.
Really nice work. Handful of pretty minor nits.
let args = | ||
unsafe { slice::from_raw_parts_mut((*cf.args).elts as *mut ngx_str_t, (*cf.args).nelts) }; | ||
|
||
let name: ngx_str_t = args[1]; |
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.
Please validate args len first
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 len is validated by the config parser before invoking the callback. If it doesn't match the flags set in the ngx_command_t
, it's totally fine to crash or fail in any other way.
I addressed this in a comment and added an assertion — likely redundant as Index ops are checked in debug.
examples/shared_dict.rs
Outdated
return; | ||
}; | ||
|
||
let value = unsafe { slice::from_raw_parts(v.data, v.len() as usize) }; |
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.
This could use a comment that indicates its semantically equivalent to the try_from_bytes_in above
src/core/rbtree.rs
Outdated
|
||
/// Raw iterator over the `ngx_rbtree_t` nodes. | ||
/// | ||
/// This iterator remains valid after removing the current item from the tree. |
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'm not sure if this struct actually needs to be pub
- it looks to me like its private to the implementation of all the other structs, but maybe I missed something. It looks like the two places this gets used its paired with a PhantomData<(K, V)> so maybe that aspect can be folded into the "Raw" iter?
The second line in the dod comment needs elaboration. What is the current item in the tree, the Item? What is the operation to remove an Item from the tree? Maybe the comment about validity to perform some specified remove operation actually belongs on the methods for RBTree::iter
and RBTree::iter_mut
, and on Iter and IterMut, if it matters to the consumers of those structs/methods.
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.
After this comment I realized that I had no clear picture when writing ngx::core::rbtree
, and thus failed in documenting the purpose.
The underlying tree is not quite a complete key-value map implementation. Sure, there is a key: ngx_rbtree_key_t
(a.k.a. usize
), and a value type which embeds the ngx_rbtree_node_t
, but RbTree<K,V,A>
type is not a safe wrapper for an arbitrary NGINX rbtree. It is an implementation of a Map<K,V>
with ngx_rbtree_t
as a building block.
RawIter
though works on any ngx_rbtree_key_t
and just accesses the tree nodes sorted by node.key
, not being aware of the value type or allocation method.
In the end, I want to implement both a generic ngx_rbtree_t
wrapper to work with references to the NGINX internal trees, and a high-level Map/Set/etc type for a pure Rust module code. I renamed things and added some comments to clarify that, but I don't believe I got the abstractions right yet.
I'll take some time to think about that while attending the conference.
The current implementation should be good enough for our internal projects though.
src/core/rbtree.rs
Outdated
_ph: PhantomData<(K, V)>, | ||
} | ||
|
||
struct Node<K, V> { |
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 may be misunderstanding exactly what this private helper is used for (it should have a comment) but if the offset_of the node field is important, should this be #[repr(C)] to make sure the offset of node
matches the C repr? It looks like the Layout of this gets passed to the allocator, do we need to guarantee that it has the same layout as the C repr?
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.
This type has no C counterpart and will not be accessed from C. AFAIU, the field offsets are stable when used within the same Rust compilation target, notwithstanding the layout or repr, so we don't need to force a specific representation.
Add some helpers for working with allocators.
Appreciate the very detailed reviews. |
Not quite at the state I wanted to reach, but should be good to start the review.
Known issues and annoyances:
NgxString
size is 4 words (ptr, len, capacity, alloc).std::string::String
is the same though, so this can be ignored for now.Hasher
impl in::core
. We may need to add another dependency or implement our own Hasher.RwLock
is just a tiniest bit evil. It supposed to work anywhere where nginx locks do, but I have doubts...I considered
pthread_rwlock_t
withpthread_rwlockattr_setpshared
, but there's that one platform without POSIX threads.RbTree
. We may consider making pure Rust implementations of the data structures with fallible allocation support as a separate crate in the future.postconfiguration
not being invoked. Not reproducible locally with up-to-date toolchain.ngx_shm_zone_t
safely.