[tor-commits] [tor/master] Topic documentation on our publish-subscribe architecture.

nickm at torproject.org nickm at torproject.org
Mon Dec 2 16:41:51 UTC 2019


commit d700dc7801a9e23ddda0d482eeb2b64ced3ca756
Author: Nick Mathewson <nickm at torproject.org>
Date:   Sat Nov 16 14:31:49 2019 -0500

    Topic documentation on our publish-subscribe architecture.
---
 src/core/mainloop/mainloop_pubsub.h |  32 ++++++++
 src/lib/pubsub/publish_subscribe.md | 141 ++++++++++++++++++++++++++++++++++++
 src/mainpage.md                     |   3 +-
 3 files changed, 175 insertions(+), 1 deletion(-)

diff --git a/src/core/mainloop/mainloop_pubsub.h b/src/core/mainloop/mainloop_pubsub.h
index bd57c0c17..c02127401 100644
--- a/src/core/mainloop/mainloop_pubsub.h
+++ b/src/core/mainloop/mainloop_pubsub.h
@@ -14,9 +14,41 @@
 
 struct pubsub_builder_t;
 
+/**
+ * Describe when and how messages are delivered on message channel.
+ *
+ * Every message channel must be associated with one of these strategies.
+ **/
 typedef enum {
+   /**
+    * Never deliver messages automatically.
+    *
+    * If a message channel uses this strategy, then no matter now many
+    * messages are published on it, they are not delivered until something
+    * manually calls dispatch_flush() for that channel
+    **/
    DELIV_NEVER=0,
+   /**
+    * Deliver messages promptly, via the event loop.
+    *
+    * If a message channel uses this strategy, then publishing a messages
+    * that channel activates an event that causes messages to be handled
+    * later in the mainloop.  The messages will be processed at some point
+    * very soon, delaying only for pending IO events and the like.
+    *
+    * Generally this is the best choice for a delivery strategy, since
+    * it avoids stack explosion.
+    **/
    DELIV_PROMPT,
+   /**
+    * Deliver messages immediately, skipping the event loop.
+    *
+    * Every event on this channel is flushed immediately after it is queued,
+    * using the stack.
+    *
+    * This delivery type should be used with caution, since it can cause
+    * unexpected call chains, resource starvation, and the like.
+    **/
    DELIV_IMMEDIATE,
 } deliv_strategy_t;
 
diff --git a/src/lib/pubsub/publish_subscribe.md b/src/lib/pubsub/publish_subscribe.md
new file mode 100644
index 000000000..019591bc1
--- /dev/null
+++ b/src/lib/pubsub/publish_subscribe.md
@@ -0,0 +1,141 @@
+
+ at page publish_subscribe Publish-subscribe message passing in Tor
+
+ at tableofcontents
+
+## Introduction
+
+Tor has introduced a generic publish-subscribe mechanism for delivering
+messages internally.  It is meant to help us improve the modularity of
+our code, by avoiding direct coupling between modules that don't
+actually need to invoke one another.
+
+This publish-subscribe mechanism is *not* meant for handing
+multithreading or multiprocess issues, thought we hope that eventually
+it might be extended and adapted for that purpose.  Instead, we use
+publish-subscribe today to decouple modules that shouldn't be calling
+each other directly.
+
+For example, there are numerous parts of our code that might need to
+take action when a circuit is completed: a controller might need to be
+informed, an onion service negotiation might need to be attached, a
+guard might need to be marked as working, or a client connection might
+need to be attached.  But many of those actions occur at a higher layer
+than circuit completion: calling them directly is a layering violation,
+and makes our code harder to understand and analyze.
+
+But with message-passing, we can invert this layering violation: circuit
+completion can become a "message" that the circuit code publishes, and
+to which higher-level layers subscribe.  This means that circuit
+handling can be decoupled from higher-level modules, and stay nice and
+simple. (@ref pubsub_notyet "1")
+
+> @anchor pubsub_notyet 1. Unfortunately, like most of our code, circuit
+> handling is _not_ yet refactored to use publish-subscribe throughout.
+> Instead, layer violations of the type described here are pretty common
+> in Tor today.  To see a small part of what happens when a circuit is
+> completed today, have a look at circuit_build_no_more_hops() and its
+> associated code.
+
+## Channels and delivery policies
+
+To work with messages, especially when refactoring existing code, you'll
+need to understand "channels" and "delivery policies".
+
+Every message is delivered on a "message channel".  Each channel
+(conceptually) a queue-like structure that can support an arbitrarily
+number of message types.  Where channels vary is their delivery
+mechanisms, and their guarantees about when messages are processed.
+
+Currently, three delivery policies are possible:
+
+   - `DELIV_PROMPT` -- causes messages to be processed via a callback in
+      Tor's event loop.  This is generally the best choice, since it
+      avoids unexpected growth of the stack.
+
+   - `DELIV_IMMEDIATE` -- causes messages to be processed immediately
+      on the call stack when they are published.  This choice grows the
+      stack, and can lead to unexpected complexity in the call graph.
+      We should only use it when necessary.
+
+   - `DELIV_NEVER` -- causes messages not to be delivered by the message
+      dispatch system at all. Instead, some other part of the code must
+      call dispatch_flush() to get the messages delivered.
+
+## Layers: Dispatch vs publish-subsubscribe vs mainloop.
+
+At the lowest level, messages are sent via the "dispatcher" module in
+ at refdir{lib/dispatch}.  For performance, this dispatcher works with a
+untyped messages.  Publishers, subscribers, channels, and messages are
+distinguished by short integers.  Associated data is handled as
+dynamically-typed data pointers, and its types are also stored as short
+integers.
+
+Naturally, this results in a type-unsafe C API, so most other modules
+shouldn't invoke @refdir{lib/dispatch} directly.  At a higher level,
+ at refdir{lib/pubsub} defines a set of functions and macros that make
+messages named and type-safe.  This is the one that other modules should
+use when they want to send or receive a message.
+
+The two modules above do not handle message delivery.  Instead, the
+dispatch module takes a callback that it can invoke when a channel
+becomes nonempty, and defines a dispatch_flush() function to deliver all
+the messages queued in a channel.  The work of actually making sure that
+dispatch_flush() is called when appropriate falls to the main loop,
+which needs to integrate the message dispatcher with the rest of our
+events and callbacks.  This work happens in mainloop_pubsub.c.
+
+
+## How to publish and subscribe
+
+This section gives an overview of how to make new messages and how to
+use them.  For full details, see pubsub_macros.h.
+
+Before anybody can publish or subscribe to a message, the message must
+be declared, typically in a header.  This uses DECLARE_MESSAGE() or
+DECLARE_MESSAGE_INT().
+
+Only subsystems can publish or subscribe messages.  For more information
+about the subsystems architecture, see @ref initialization.
+
+To publish a message, you must:
+   - Include the header that declares the message.
+   - Declare a set of helper functions via DECLARE_PUBLISH().  These
+     must be visible wherever you call PUBLISH().
+   - Call PUBLISH() to actually send a message.
+   - Connect your subsystem to the dispatcher by calling
+     DISPATCH_ADD_PUB() from your subsystem's subsys_fns_t.add_pubsub
+     callback.
+
+To subscribe to a message, you must:
+   - Include the header that declares the message.
+   - Declare a callback function to be invoked when the message is delivered.
+   - Use DISPATCH_SUBSCRIBE at file scope to define a set of wrapper
+     functions to call your callback function with the appropriate type.
+   - Connect your subsystem to the dispatcher by calling
+     DISPATCH_ADD_SUB() from your subsystem's subsys_fns_t.add_pubsub
+     callback.
+
+Again, the file-level documentation for pubsub_macros.h describes how to
+declare a message, how to publish it, and how to subscribe to it.
+
+## Designing good messages
+
+**Frequency**:
+The publish-subscribe system uses a few function calls
+and allocations for each message sent. This makes it unsuitable for
+very-high-bandwidth events, like "receiving a single data cell" or "a
+socket has become writable."  It's fine, however, for events that
+ordinarily happen a bit less frequently than that, like a circuit
+getting finished, a new connection getting opened, or so on.
+
+**Semantics**:
+A message should declare that something has happened or is happening,
+not that something in particular should be done.
+
+For example, suppose you want to set up a message so that onion services
+clean up their replay caches whenever we're low on memory.  The event
+should be something like `memory_low`, not `clean_up_replay_caches`.
+The latter name would imply that the publisher knew who was subscribing
+to the message and what they intended to do about it, which would be a
+layering violation.
diff --git a/src/mainpage.md b/src/mainpage.md
index a2c5ec630..63a5b0a3f 100644
--- a/src/mainpage.md
+++ b/src/mainpage.md
@@ -41,6 +41,8 @@ Tor repository.
 
 @subpage time_periodic
 
+ at subpage publish_subscribe
+
 
 @page intro A high-level overview
 
@@ -140,4 +142,3 @@ more connection types.
 
 A 'Node' (node_t) is a view of a Tor instance's current knowledge and opinions
 about a Tor relay or bridge.
-





More information about the tor-commits mailing list