-
Notifications
You must be signed in to change notification settings - Fork 201
ENT-14195 0. README for cf-reactor event handling #6352
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
Merged
+82
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| # cf-reactor | ||
|
|
||
| # High level design | ||
|
|
||
| ### Tracking | ||
|
|
||
| cf-reactor periodically reads the policy to set up the in-memory datastructures needed to react to events. It evaluates reactor bundles and parses events promises to create the corresponding `watchers`. These watchers stay active until the next policy read, when they are rebuilt. | ||
|
|
||
| Events promises are parsed into watcher instances, each holding: the event `key` from the events promise, an event `type` (e.g. file deletion), a `payload` with whatever state the check needs (e.g. the name of the file being watched), and a `check function` that detects the event by comparing the current state to the state recorded at the last check (e.g. whether the file still exists). It returns `true` only on the specific transition being watched for, so it fires once per change and does not stay `true` afterwards. | ||
|
|
||
| ### Events | ||
|
|
||
| The daemon runs a loop that calls `select(2)` on its file descriptors until one of them signals activity. When a check function fires, it pushes the event key onto a thread-safe `event queue` and writes to the reactor's file descriptor, waking up the `select(2)` loop. This means that for now, an event is just a string. | ||
|
|
||
| On wakeup, the daemon drains the event queue and dispatches each event to its bundle using a `hashmap`, built from the policy, that maps event keys to bundles. | ||
|
|
||
| ### Polling | ||
|
|
||
| To remain cross-platform, cf-reactor detects changes with a simple polling thread: it iterates through all watcher instances and calls their check functions to see whether a change happened, then sleeps. | ||
|
|
||
| On Linux, we could use inotify instead in the future, and let the kernel poll for us. Its architecture is similar to ours: a single file descriptor is written to on an event, and events are pushed to a queue that must be read. The difference is that inotify maps events to actions using "watch descriptors" (an int) rather than keys, so we would need a translation layer between watch descriptors and event keys. | ||
|
|
||
| ### Running bundles | ||
|
|
||
| On dispatch, the bundle corresponding to an event is run, however it doesn't do a full agent run. | ||
|
|
||
| ## Implementation details | ||
|
|
||
| ### Moving stuff around | ||
|
|
||
| Rather than exposing the raw file-descriptor bookkeeping required for `select(2)`, we introduce a unified interface that serves both the reactor-plugin and event-driven code paths. This is achieved by encapsulating all relevant state in a context struct, `ReactorContext`: | ||
|
|
||
| ```C | ||
| typedef struct ReactorContext | ||
| { | ||
| int *all_fds; // heap allocated array of fds | ||
| size_t all_fds_capacity; // total number of fds. number of nova fds + number of event fds | ||
| size_t num_nova_fds; // this is returned by the reactor-plugin | ||
| size_t num_fds; // this is 1 | ||
| // the first (num_nova_fds - 1) slots in the array are reserved for the reactor-plugin, the last one is reserved for the event driven code. | ||
|
|
||
| fd_set readfds; | ||
| } ReactorContext; | ||
| ``` | ||
|
|
||
| - `ReactorContextInitialize()`: initializes the reactor-plugin and event-driven code. Wraps `ReactorNovaInitialize()` | ||
| - `ReactorContextSetupFileDescriptors()`: populates readfds with the file descriptors to monitor, prior to the select() call. | ||
| - `ReactorContextHandleEvents()`: iterates over the file descriptors and dispatches the appropriate action based on which ones were signaled as ready. Wraps `ReactorNovaHandleTimeout` and `ReactorNovaHandleEvents()`. | ||
| - `ReactorContextFinalize()`: releases the daemon's associated resources. Wraps `ReactorNovaFinalize()`. | ||
|
|
||
| ### Tracking spec & Events | ||
|
|
||
| In order to track all the events promises, we use two datastructures: a global list of `"Watcher"`, which is a struct associated with an event type and the promise name (also called `key`) and a global hashmap mapping this `key` to a `bundle` which is parsed from the policy. | ||
|
|
||
| cf-reactor reads the policy periodically, and when it does, rebuilds the list of watchers and the hashmap using the single function `WatcherRegister(key, event_type, payload, bundle, interval)`. Each events promise is associated with an event type, which is defined in `when` bodies: | ||
|
|
||
| ```cf3 | ||
| body when file_deleted(filename) | ||
| { | ||
| file_deleted => "$(filename)"; | ||
| } | ||
| ``` | ||
|
|
||
| Every event type must have defined: | ||
| - A check function (called `check_fn` in `Watcher`): This is a function defined specifically for the event that checks if the conditions holds. For example, in case of file deletion, we check if the file doesn't exist anymore compare to the last time we checked. If yes, then it returns `true`. | ||
|
victormlg marked this conversation as resolved.
|
||
| - A `payload`: This is a struct whose interpretation depends on the event type (thus being declared as `void *`). We typically need some state that we compare between each event-check. In the case of file deletion, we need to know the name of the file we are watching, and whether the file existed last time we checked. | ||
| - A payload destroying function (called `destroy_payload`): This is simply a function to free the payload associated with the event type. | ||
|
|
||
| Also, we need a function that will create the payload. That's what `FileWatcherPayloadNew()` does. | ||
|
|
||
| So each event type we add in the future just need to have these four things defined, and we need to create the `Watcher` object with the right functions inside `WatcherRegister()` and also call the right `"...PayloadNew()"` function. | ||
|
|
||
|
|
||
| ### Polling & Running bundles | ||
|
|
||
| `ReactorContextInitialize()` sets up all the necessary data structures for polling, and then starts `WatcherThreadMain`, which polls for events as follows: | ||
|
|
||
| - It iterates through each watcher in the global list of watchers. | ||
| - If the elapsed time exceeds the watcher's `interval`, it runs `check_fn` to determine whether an event has been triggered. | ||
| - If an event was triggered, it pushes the watcher's `key` (the promise name) onto a thread-safe queue, then signals the file descriptor via `WakeupChannelNotify`, which `select(2)` will pick up on its next iteration. It then goes back to sleep. | ||
|
|
||
| In parallel, `EventWatcherHandleEvents`, called from within `ReactorContextHandleEvents`, reads from the file descriptor with `WakeupChannelReadFd()` once notified that an event has occurred, and pops the thread-safe queue until it's empty. Each key popped from the queue is looked up in the global hashmap to retrieve the corresponding bundle, which `cf-reactor` then runs (in another thread or subprocess) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Would it make sense to have two
Seq *, one for reactor-plugin FDs, and one for the other FDs? Or perhaps oneSeq *where FDs are wrapped in a struct with flags determining who owns them?