Project: gRPC (Orchestration & Management / Remote Procedure Call)
File: grpc-polling-engines.md

Author: Sree Kuchibhotla (@sreecha) - Sep 2018 Polling engine component was created for the following reasons: gRPC code deals with a bunch of file descriptors on which events like descriptor being readable/writable/error have to be monitored gRPC code knows the actions to perform when such events happen For example: `grpc_endpoint` code calls `recvmsg` call when the fd is readable and `sendmsg` call when the fd is writable ` tcp_client` connect code issues async `connect` and finishes creating the client once the fd is writable (i.e when the `connect` actually finished) gRPC needed some component that can "efficiently" do the above operations using the threads provided by the applications (i.e., not create any new threads). Also by "efficiently" we mean optimized for latency and throughput There are multiple polling engine implementations depending on the OS and the OS version. Fortunately all of them expose the same interface Linux: `epoll1` (If glibc version >= 2.9) `poll` (If kernel does not have epoll support) Mac: `poll` (default) Windows: (no name) The following are the Opaque structures exposed by Polling Engine interface (NOTE: Different polling engine implementations have different definitions of these structures) grpc_fd: Structure representing a file descriptor grpc_pollset: A set of one or more grpcfds that are polled for readable/writable/error events. One grpcfd can be in multiple `grpc_pollset`s grpc_pollset_worker: Structure representing a polling thread - more specifically, the thread that calls `grpcpollsetwork()` API grpc_pollset_set: A group of `grpcfd`s, `grpcpollset`s and `grpcpollsetset`s (yes, a `grpcpollsetset` can contain other `grpcpollsetset`s) grpc\_fd\_notify\_on\_[read|write|error] Signature: `grpcfdnotifyon(grpcfd* fd, grpcclosure* closure)` Register a to be called when the fd becomes readable/writable or has an error (In grpc parlance, we refer to this act as arming the fd) The closure is called exactly once per event. I.e once the fd becomes readable (or writable or error), the closure is fired and the fd is unarmed. To be notified again, the fd has to be armed again. grpc_fd_shutdown Signature: `grpcfdshutdown(grpc_fd* fd)` Any current (or future) closures registered for readable/writable/error events are scheduled immediately with an error grpc_fd_orphan Signature: `grpcfdorphan(grpcfd* fd, grpcclosure on_done, int release_fd, char* reason)` Release the `grpcfd` structure and call `ondone` closure when the operation is complete If `releasefd` is set to `nullptr`, then `close()` the underlying fd as well. If not, put the underlying fd in `releasefd` (and do not call `close()`) `release_fd` set to non-null in cases where the underlying fd is NOT owned by grpc core (like for example the fds used by C-Ares DNS resolver ) grpc_pollset_add_fd Signature: `grpcpollsetaddfd(grpcpollset ps, grpc_fd fd)` Add fd to pollset > NOTE: There is no `grpcpollsetremovefd`. This is because calling `grpcfd_orphan()` will effectively remove the fd from all the pollsets its a part of grpc_pollset_work Signature: `grpcpollsetwork(grpcpollset* ps, grpcpollsetworker** worker, grpccore::Timestamp deadline)` > NOTE: `grpcpollsetwork()` requires the pollset mutex to be locked before calling it. Shortly after calling `grpcpollsetwork()`, the function populates the `worker` pointer (among other things) and releases the mutex. Once `grpc_pollset_work()` returns, the `worker` pointer is invalid and should not be used anymore. See the code in `completion_queue.cc` to see how this is

Poll the fds in the pollset for events AND return when ANY of the following is true: Deadline expired Some fds in the pollset were found to be readable/writable/error and those associated closures were scheduled (but not necessarily executed) worker is kicked (see `grpcpollsetkick` for more details) grpc_pollset_kick Signature: `grpcpollsetkick(grpcpollset* ps, grpcpollset_worker* worker)` Kick the worker i.e Force the worker to return from grpcpollsetwork() If `worker == nullptr`, kick ANY worker active on that pollset grpc\_pollset\_set\_[add|del]\_fd Signature: `grpcpollsetset[add|del]fd(grpcpollsetset pss, grpc_fd fd)` Add/Remove fd to the `grpcpollsetset` grpc\_pollset\_set_[add|del]\_pollset Signature: `grpcpollsetset[add|del]pollset(grpcpollsetset pss, grpc_pollset ps)` What does adding a pollset to a pollset_set mean ? It means that calling `grpcpollsetwork()` on the pollset will also poll all the fds in the pollsetset i.e semantically, it is similar to adding all the fds inside pollsetset to the pollset. This guarantee is no longer true once the pollset is removed from the pollset_set grpc\_pollset\_set_[add|del]\_pollset\_set Signature: `grpcpollsetset[add|del]pollsetset(grpcpollsetset* bag, grpcpollset_set* item)` Semantically, this is similar to adding all the fds in the bag pollsetset to the item pollsetset Relation between grpc_pollset_worker, grpc_pollset and grpc_fd: grpc_pollset_set Code at `src/core/lib/iomgr/evepoll1posix.cc` The logic to choose a designated poller is quite complicated. Pollsets are internally sharded into what are called `pollsetneighborhood` (a structure internal to `epoll1` polling engine implementation). `grpcpollsetworkers` that call `grpcpollsetwork` on a given pollset are all queued in a linked-list against the `grpcpollset`. The head of the linked list is called "root worker" There are as many neighborhoods as the number of cores. A pollset is put in a neighborhood based on the CPU core of the root worker thread. When picking the next designated poller, we always try to find another worker on the current pollset. If there are no more workers in the current pollset, a `pollset_neighborhood` listed is scanned to pick the next pollset and worker that could be the new designated poller. NOTE: There is room to tune this implementation. All we really need is good way to maintain a list of `grpcpollsetworkers` with a way to group them per-pollset (needed to implement `grpcpollsetkick` semantics) and a way randomly select a new designated poller See function to see how a designated poller is chosen. Similarly function is called by the worker that was just out of `epoll_wait()` and will have to choose a new designated poller) poll polling engine: gRPC's `poll` polling engine is quite complicated. It uses the `poll()` function to do the polling (and hence it is for platforms like osx where epoll is not available) The implementation is further complicated by the fact that poll() is level triggered (just keep this in mind in case you wonder why the code at `src/core/lib/iomgr/evpollposix.cc` is written a certain/seemingly complicated way :)) Polling engine on Windows: Windows polling engine looks nothing like other polling engines Unlike the grpc polling engines for Unix systems (epoll1 and poll) Windows endpoint implementation and polling engine implementations are very closely tied together Windows endpoint read/write API implementations use the Windows IO API which require specifying an In Windows polling engines grpcpollsetwork() implementation, ONE of the threads is chosen to wait on the I/O completion port while other threads wait on a condition variable (much like the turnstile polling in epoll1)