Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion cf-reactor/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ libcf_reactor_la_LIBADD = ../libpromises/libpromises.la

libcf_reactor_la_SOURCES = \
cf-reactor.c \
reactor_context.c reactor_context.h
reactor_context.c reactor_context.h \
reactor_transform.c reactor_transform.h \
watcher.c watcher.h \
wakeup_channel.c wakeup_channel.h

if !BUILTIN_EXTENSIONS
bin_PROGRAMS = cf-reactor
Expand Down
31 changes: 7 additions & 24 deletions cf-reactor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

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 promises are parsed into watcher instances, each holding: the event `key` from the events promise, an event `type` (e.g. file deletion), a `state` 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

Expand All @@ -26,28 +26,11 @@ On dispatch, the bundle corresponding to an event is run, however it doesn't do

## 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
{
Seq *fds // array of ReactorFd, which holds the fd and some metadata
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:
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, state, bundle, interval)`. Each events promise is associated with an event type, which is defined in `when` bodies:

```cf3
body when file_deleted(filename)
Expand All @@ -57,11 +40,11 @@ body when 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`.
- 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.
- A check function (called `check_callback` 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`.
- A `state`: 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 state destroying function (called `destroy_state`): This is simply a function to free the state associated with the event type.

Also, we need a function that will create the payload. That's what `FileWatcherPayloadNew()` does.
Also, we need a function that will create the state. 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.

Expand All @@ -71,7 +54,7 @@ So each event type we add in the future just need to have these four things defi
`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 the elapsed time exceeds the watcher's `interval`, it runs `check_callback` 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)
101 changes: 94 additions & 7 deletions cf-reactor/cf-reactor.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,17 @@
#include <writer.h>
#include <config.h>
#include <generic_agent.h>
#include <loading.h> /* LoadPolicy */
#include <bootstrap.h> /* UpdateLastPolicyUpdateTime */
#include <known_dirs.h> /* GetInputDir */
#include <man.h>
#include <cleanup.h>
#include <prototypes3.h>
#include <signal.h> /* signal, kill */
#include <signals.h> /* GetSignalPipe, MakeSignalPipe, IsPendingTermination, HandleSignalsForDaemon */
#include <signals.h> /* GetSignalPipe, MakeSignalPipe, IsPendingTermination, HandleSignalsForDaemon, ReloadConfigRequested, ClearRequestReloadConfig */
#include <exec_tools.h>
#include <reactor_context.h>
#include <reactor_transform.h>

/*****************************************************************************/
/* Globals */
Expand All @@ -49,6 +53,7 @@
/*****************************************************************************/

#define DEFAULT_POLL_INTERVAL_SECS 30
#define DEFAULT_POLICY_CHECK_INTERVAL_SECS (5 * 60)

/*******************************************************************/
/* Command line options */
Expand All @@ -70,6 +75,7 @@
static const struct option OPTIONS[] =
{
{"debug", no_argument, 0, 'd'},
{"file", required_argument, 0, 'f'},
{"no-fork", no_argument, 0, 'F'},
{"log-level", required_argument, 0, 'g'},
{"help", no_argument, 0, 'h'},
Expand All @@ -84,6 +90,7 @@
static const char *const HINTS[] =
{
"Enable debugging output and run in foreground",
"Specify an alternative input file than the default. This option is overridden by FILE if supplied as argument.",
"Run as a foreground process (do not fork)",
"Specify how detailed logs should be. Possible values: 'error', 'warning', 'notice', 'info', 'verbose', 'debug'",
"Print the help message",
Expand All @@ -102,11 +109,15 @@
GenericAgentConfig *config = GenericAgentConfigNewDefault(AGENT_TYPE_REACTOR, GetTTYInteractive());

int longopt_idx;
while ((c = getopt_long(argc, argv, "dFg:hIlMvV",
while ((c = getopt_long(argc, argv, "df:Fg:hIlMvV",
OPTIONS, &longopt_idx)) != -1)
{
switch (c)
{
case 'f':
GenericAgentConfigSetInputFile(config, GetInputDir(), optarg);
MINUSF = true;
break;
case 'd':
LogSetGlobalLevel(LOG_LEVEL_DEBUG);
NO_FORK = true;
Expand Down Expand Up @@ -190,13 +201,72 @@

/*****************************************************************************/

static void CheckPolicyUpdates(EvalContext *ctx, Policy **policy, GenericAgentConfig *config)
{
time_t validated_at = ReadTimestampFromPolicyValidatedFile(config, NULL);

bool reload_policy = false;
if (config->agent_specific.daemon.last_validated_at < validated_at)
{
Log(LOG_LEVEL_VERBOSE, "New promises detected, reloading policy");
reload_policy = true;
}
if (ReloadConfigRequested())
{
Log(LOG_LEVEL_VERBOSE, "Policy reload requested, reloading policy");
reload_policy = true;
}

if (!reload_policy)
{
Log(LOG_LEVEL_DEBUG, "No new promises found");
return;
}

ClearRequestReloadConfig();
config->agent_specific.daemon.last_validated_at = validated_at;

if (!GenericAgentArePromisesValid(config))
{
Log(LOG_LEVEL_INFO, "Reactor policy has errors, keeping the previous policy");
return;
}

Log(LOG_LEVEL_NOTICE, "Rereading policy file '%s'", config->input_file);

EvalContextClear(ctx);
PolicyDestroy(*policy);
*policy = NULL;

UpdateLastPolicyUpdateTime(ctx);
GenericAgentDiscoverContext(ctx, config, NULL);

*policy = LoadPolicy(ctx, config);
}

/*****************************************************************************/


int main(int argc, char *argv[])
{
GenericAgentConfig *config = CheckOpts(argc, argv);
EvalContext *ctx = EvalContextNew();
GenericAgentConfigApply(ctx, config);

const char *program_invocation_name = argv[0];
const char *last_dir_sep = strrchr(program_invocation_name, FILE_SEPARATOR);
const char *program_name = (last_dir_sep != NULL ? last_dir_sep + 1 : program_invocation_name);
GenericAgentDiscoverContext(ctx, config, program_name);

Policy *policy = SelectAndLoadPolicy(config, ctx, false, false);
if (!policy)
{
Log(LOG_LEVEL_ERR, "Error reading CFEngine policy. Exiting...");
DoCleanupAndExit(EXIT_FAILURE);
}

GenericAgentPostLoadInit(ctx);

#ifdef __MINGW32__

if (!NO_FORK)
Expand Down Expand Up @@ -253,15 +323,21 @@
/* We need an initial value here for the first iteration of the cycle
* below. */
time_t next_tick = time(NULL) + DEFAULT_POLL_INTERVAL_SECS;
time_t next_policy_check = time(NULL) + DEFAULT_POLICY_CHECK_INTERVAL_SECS;

// Setup event watchers from policy
KeepReactorPromises(ctx, policy);

while (!IsPendingTermination())
{
int max_fd = ReactorContextSetupFileDescriptors(&reactor_ctx);

/* Determine how much time is remaining until the next tick. */
time_t last_tick = time(NULL);
time_t remaining = next_tick > last_tick ? next_tick - last_tick : 0;
time_t remaining_tick = next_tick > last_tick ? next_tick - last_tick : 0;
time_t remaining_policy_check = next_policy_check > last_tick ? next_policy_check - last_tick : 0;

struct timeval timeout = { .tv_sec = remaining };
struct timeval timeout = { .tv_sec = MIN(remaining_tick, remaining_policy_check) };
int ret = select(max_fd, &reactor_ctx.readfds, NULL, NULL, &timeout);

/* Reschedule the backstop tick against the current time (not
Expand Down Expand Up @@ -290,14 +366,25 @@
{
/*** timeout ***/
ReactorNovaHandleTimeout(&next_tick);
continue;
}
/* else */
else
{
ReactorContextHandleEvents(&reactor_ctx, &next_tick);
}


time_t now = time(NULL);
if (now >= next_policy_check || ReloadConfigRequested())
{
CheckPolicyUpdates(ctx, &policy, config);
next_policy_check = now + DEFAULT_POLICY_CHECK_INTERVAL_SECS;

ReactorContextHandleEvents(&reactor_ctx, &next_tick);
KeepReactorPromises(ctx, policy);
}
}
ReactorContextFinalize(&reactor_ctx);

PolicyDestroy(policy);
GenericAgentFinalize(ctx, config);
CallCleanupFunctions();

Expand Down
Loading
Loading