Skip to content

Conversation

@pschyska
Copy link

@pschyska pschyska commented Oct 2, 2025

Proposed changes

As mentioned in #110 my POC for async via nginx-notify.
This would enable schedule() to be called from other threads, e.g. async-compat or other "sidecar-runtime" setups. It also makes sure epoll is interrupted when there are IO completion notifications coming in from outside of the event loop, leading to prompt continuation.
While this doesn't provide a native hyper/client as @bavshin-f5 wanted, it makes the default tokio implementation work via Compat. This would be a viable stopgap solution for us. I've added some examples, including hyper and reqwest. In the future, one could implement a "sidecar-runtime" approach as in async-compat natively that would use a separate epoll loop in a thread, or inject additional fds from the Rust side to nginx's epoll instance (if possible).

Some notes:

  • only works for event modules supporting ngx_notify, afaik this includes epoll, kqueue, eventport. Thread pools seem to have the same limitation, so this might be fine
  • not compatible with no_std right now: OnceLock (might be replaceable by something from spin) and crossbeam-channel, and probably more. I've added std as a dependency for async to reflect that (this would be a breaking change, but async Rust probably implies std anyways).

Checklist

Before creating a PR, run through this checklist and mark each as complete.

  • I have written my commit messages in the Conventional Commits format.
  • I have read the CONTRIBUTING doc
  • I have added tests (when possible) that prove my fix is effective or that my feature works (don't think it's possible)
  • I have checked that all unit tests pass after adding my changes
  • I have updated necessary documentation
  • I have rebased my branch onto main
  • I will ensure my PR is targeting the main branch and pulling from my branch from my own fork

@pschyska pschyska force-pushed the main branch 2 times, most recently from e327e07 to e1c9191 Compare October 6, 2025 13:06
@pschyska pschyska changed the title RFC: thread-safe spawn with ngx_notify thread-safe spawn with ngx_notify Oct 6, 2025
@pschyska pschyska force-pushed the main branch 4 times, most recently from 1dcea34 to 53f60c3 Compare October 6, 2025 19:34
schedule() can now be called from any thread and will move tasks to the event loop
thread using ngx_notify (ngx_event_actions.notify). This enables receiving I/O
notification from "sidecar runtimes" like async-compat, and requires less unsafe.

The async example has been rewritten to use async_::spawn, demonstrating usage of
reqwest and hyper clients wrapped in Compat to provide a tokio runtime environment while
using the async_ Scheduler as executor.
@bavshin-f5
Copy link
Member

  • ngx_notify is "thread-safe" under a very narrow set of conditions. One of those is that nobody outside of the nginx internal code is allowed to call it.
    Check ngx_epoll_module.c:769 and consider what would happen if multiple modules will start invoking ngx_notify() with different handler methods.
  • I don't want to allow mixing internal and external async runtimes or encourage use of threads. Both seem to be fragile and dangerous.
    I don't even believe you need to mix both runtimes: if you intend to use tokio, just run all the asynchronous code in the tokio task.
  • This change would break or make significantly slower any IO implementation that properly integrates with the nginx event loop (such as hyper client in nginx-acme).

@pschyska
Copy link
Author

pschyska commented Nov 7, 2025

  • ngx_notify is "thread-safe" under a very narrow set of conditions. One of those is that nobody outside of the nginx internal code is allowed to call it.
    Check ngx_epoll_module.c:769 and consider what would happen if multiple modules will start invoking ngx_notify() with different handler methods.

I see it now.

  • I don't want to allow mixing internal and external async runtimes or encourage use of threads. Both seem to be fragile and dangerous.

If it's guaranteed that all tasks run on the main thread, I don't think it's dangerous. This change only allows scheduling from other threads. It's not uncommon that libraries start their own helper threads, for instance. async-compat starts a transparent tokio runtime in a thread for IO completion handlers, while still using our executor for the tasks.

I also can image situations where you'd want to start non-IO compute in a thread pool to not block nginx - in our case, for example, crypto. You'd want to be able to notify the request handler async task of completion by writing to a channel or a similar mechanism. This, in turn, would call the waker from that thread (AFAIK), which calls schedule for the task from that thread, but the woken task would be scheduled to run on the main thread via ngx_notify.

I don't even believe you need to mix both runtimes: if you intend to use tokio, just run all the asynchronous code in the tokio task.

We need to work with the request heavily (mutate headers_in and headers_out, read client bodies, produce response bodies) in response to I/O (external requests, database queries, custom crypto/tunneling), which can only be done on the main thread safely. If all our code is running in a completely separate engine, it all becomes extremely hard. In addition, we need a way to interrupt nginx' epoll reacting I/O events, which aren't all bound to a request (OpenID shared signals, e.g.).
async-compat seemed like a good compromise to me: use the tokio "runtime" (I/O setup,...) , but with the ngx-rust scheduler/executor.

  • This change would break or make significantly slower any IO implementation that properly integrates with the nginx event loop (such as hyper client in nginx-acme).

I don't think it would do that. If the waker is invoked from the main thread, schedule in my branch would simply .run() the runnable, and everything stays on the main thread. ngx_notify would not be called (except once during the lifetime of a worker process because it's not known which tid is main). I have to admit I didn't test with nginx-acme yet though.

To recap, I'd still like the following:

  • A way to interrupt epoll
  • A way to move tasks to the main thread
  • Safe to call schedule from other threads

Given ngx_epoll_module.c:769, ngx_notify from other threads is indeed inherently unsafe.

However, what if we do this:

  • ngx_post_event a custom event, its handler being notify_handler
  • write(notify_fd, &inc, sizeof(uint64_t)) to interrupt epoll. The event loop would then find our custom event promptly.

Would this work for you?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants