diff --git a/cf-reactor/Makefile.am b/cf-reactor/Makefile.am index 710befaef0c..4036d272376 100644 --- a/cf-reactor/Makefile.am +++ b/cf-reactor/Makefile.am @@ -26,6 +26,7 @@ noinst_LTLIBRARIES = libcf-reactor.la AM_CPPFLAGS = -I$(srcdir)/../libpromises -I$(srcdir)/../libntech/libutils \ -I$(srcdir)/../libcfecompat \ -I$(srcdir)/../libcfnet \ + -I$(srcdir)/../libenv \ -I$(srcdir)/../cf-agent \ $(OPENSSL_CPPFLAGS) \ $(PCRE2_CPPFLAGS) \ diff --git a/cf-reactor/reactor_transform.c b/cf-reactor/reactor_transform.c index 6660a8308aa..d5b116d6bed 100644 --- a/cf-reactor/reactor_transform.c +++ b/cf-reactor/reactor_transform.c @@ -38,6 +38,8 @@ #include #include // ConnCache_Init(), ConnCache_Destroy() #include // Initialize/FinalizeCustomPromises() +#include // SetReferenceTime() +#include // UpdateTimeClasses() /* Promise types evaluated within `bundle reactor NAME { ... }`. */ static const char *const REACTOR_TYPESEQUENCE[] = @@ -258,6 +260,16 @@ void HandleReactorEvent(EvalContext *ctx, const Promise *pp, const char *promise assert(pp != NULL); assert(promiser != NULL); + /* cf-reactor keeps the same EvalContext across events. Like an agent run, + * the bundle runs see the time classes of now and the persistent classes + * that have not expired. The classes and variables they define, cancel or + * change are undone once the event is handled, except the persistent + * classes, which are kept in the state database. */ + UpdateTimeClasses(ctx, SetReferenceTime()); + ClassTable *classes = NULL; + VariableTable *variables = NULL; + EvalContextSnapshotTake(ctx, &classes, &variables); + ReactorEventParam event = { .promiser = promiser }; EvalContextStackPushBundleFrame(ctx, PromiseGetBundle(pp), NULL, false, NULL); @@ -265,6 +277,8 @@ void HandleReactorEvent(EvalContext *ctx, const Promise *pp, const char *promise ExpandPromise(ctx, pp, KeepEventsPromiseOnEvent, &event); EvalContextStackPopFrame(ctx); /* bundle section */ EvalContextStackPopFrame(ctx); /* bundle */ + + EvalContextSnapshotRestore(ctx, classes, variables); } void KeepReactorPromises(EvalContext *ctx, const Policy *policy) @@ -289,4 +303,9 @@ void KeepReactorPromises(EvalContext *ctx, const Policy *policy) EvaluateReactorBundle(ctx, bp); } + + /* Tag the persistent classes the evaluation defined 'source=persistent', + * so that the snapshots taken on events tell them apart even once another + * agent deleted them from the state database, see EvalContextSnapshotTake() */ + EvalContextHeapPersistentLoadAll(ctx); } diff --git a/libpromises/class.c b/libpromises/class.c index dcd4f5c6bd8..df4f7492d99 100644 --- a/libpromises/class.c +++ b/libpromises/class.c @@ -231,6 +231,26 @@ bool ClassTableClear(ClassTable *table) return has_classes; } +ClassTable *ClassTableCopy(const ClassTable *table) +{ + assert(table != NULL); + + ClassTable *copy = ClassTableNew(); + + ClassTableIterator *iter = ClassTableIteratorNew(table, NULL, true, true); + for (const Class *cls = ClassTableIteratorNext(iter); cls != NULL; + cls = ClassTableIteratorNext(iter)) + { + /* The tags of a class are never NULL, see ClassInit() */ + StringSet *tags = StringSetNew(); + StringSetJoin(tags, cls->tags, xstrdup); + ClassTablePut(copy, cls->ns, cls->name, cls->is_soft, cls->scope, tags, cls->comment); + } + ClassTableIteratorDestroy(iter); + + return copy; +} + ClassTableIterator *ClassTableIteratorNew(const ClassTable *table, const char *ns, bool is_hard, bool is_soft) diff --git a/libpromises/class.h b/libpromises/class.h index 67b1d8cb209..918f0df25d6 100644 --- a/libpromises/class.h +++ b/libpromises/class.h @@ -53,6 +53,12 @@ bool ClassTableRemove(ClassTable *table, const char *ns, const char *name); bool ClassTableClear(ClassTable *table); +/** + * @brief Deep copy of the table: all its classes, hard and soft, with their + * tags and comments. The copy is owned by the caller. + */ +ClassTable *ClassTableCopy(const ClassTable *table); + ClassTableIterator *ClassTableIteratorNew(const ClassTable *table, const char *ns, bool is_hard, bool is_soft); Class *ClassTableIteratorNext(ClassTableIterator *iter); void ClassTableIteratorDestroy(ClassTableIterator *iter); diff --git a/libpromises/eval_context.c b/libpromises/eval_context.c index 2fbe3ffddb8..fcabe0cf76e 100644 --- a/libpromises/eval_context.c +++ b/libpromises/eval_context.c @@ -893,6 +893,105 @@ void EvalContextHeapPersistentLoadAll(EvalContext *ctx) CloseDB(dbp); } +/*****************************************************************************/ + +/* Whether the class is persistent, expired or not: loaded from the state + * database, which tags it 'source=persistent', or defined by a persistent + * class promise, which saves it in the database without tagging it. The keys + * are the class names as ClassRefToString() makes them, see + * EvalContextHeapPersistentSave(). dbp may be NULL if the database cannot be + * opened. */ +static bool IsPersistentClass(CF_DB *dbp, const Class *cls) +{ + assert(cls != NULL); + + if (StringSetContains(cls->tags, "source=persistent")) + { + return true; + } + if (dbp == NULL) + { + return false; + } + + char *key = ClassRefToString(cls->ns, cls->name); + const bool persistent = HasKeyDB(dbp, key, strlen(key) + 1); + free(key); + return persistent; +} + +void EvalContextSnapshotTake(EvalContext *ctx, ClassTable **classes, VariableTable **variables) +{ + assert(ctx != NULL); + assert(classes != NULL); + assert(variables != NULL); + + /* 1. Copy the global classes and variables, the hard classes and the + * special variables too. The bundle classes and variables are not in + * the global tables, they disappear with the frame of their bundle. */ + *classes = ClassTableCopy(ctx->global_classes); + *variables = VariableTableCopy(ctx->global_variables); + + /* 2. Remove the persistent classes, expired or not, from both the copy and + * the context: their lifetime is their TTL in the state database, not + * the snapshot nor the context, which may have outlived it. Collect + * first, the context must not change while being iterated. */ + CF_DB *dbp; + if (!OpenDB(&dbp, dbid_state)) + { + dbp = NULL; + } + + Seq *persistent_classes = SeqNew(16, free); + /* ClassTableIteratorNext() does not filter the hard classes out */ + ClassTableIterator *iter = ClassTableIteratorNew(ctx->global_classes, NULL, false, true); + for (const Class *cls = ClassTableIteratorNext(iter); cls != NULL; + cls = ClassTableIteratorNext(iter)) + { + if (cls->is_soft && IsPersistentClass(dbp, cls)) + { + ClassTableRemove(*classes, cls->ns, cls->name); + SeqAppend(persistent_classes, ClassRefToString(cls->ns, cls->name)); + } + } + ClassTableIteratorDestroy(iter); + + if (dbp != NULL) + { + CloseDB(dbp); + } + + for (size_t i = 0; i < SeqLength(persistent_classes); i++) + { + ClassRef ref = ClassRefParse(SeqAt(persistent_classes, i)); + ClassTableRemove(ctx->global_classes, ref.ns, ref.name); + ClassRefDestroy(ref); + } + SeqDestroy(persistent_classes); + + /* 3. Load the persistent classes that have not expired into the context, + * like an agent does when it starts. The expired ones are deleted from + * the database instead. */ + EvalContextHeapPersistentLoadAll(ctx); +} + +void EvalContextSnapshotRestore(EvalContext *ctx, ClassTable *classes, VariableTable *variables) +{ + assert(ctx != NULL); + assert(classes != NULL); + assert(variables != NULL); + + /* Replace the global classes and variables with the snapshot, which + * discards the ones defined, cancelled or changed since, the persistent + * classes too: the next snapshot loads them again from the state + * database, see EvalContextSnapshotTake(). */ + ClassTableDestroy(ctx->global_classes); + ctx->global_classes = classes; + + VariableTableDestroy(ctx->global_variables); + ctx->global_variables = variables; +} + void EvalContextSetNegatedClasses(EvalContext *ctx, StringSet *negated_classes) { assert(ctx != NULL); diff --git a/libpromises/eval_context.h b/libpromises/eval_context.h index 2537c083f65..721c017353b 100644 --- a/libpromises/eval_context.h +++ b/libpromises/eval_context.h @@ -145,6 +145,30 @@ void EvalContextHeapPersistentSave(EvalContext *ctx, const char *name, unsigned void EvalContextHeapPersistentRemove(const char *context); void EvalContextHeapPersistentLoadAll(EvalContext *ctx); +/** + * @brief Save a copy of the global classes, except the persistent ones, and + * of the global variables, so that EvalContextSnapshotRestore() can + * undo what is evaluated in between. Then reset the persistent classes + * of the context to the ones in the state database that have not + * expired, as an agent starting a run does. + * + * @param classes set to the copy of the classes, owned by the caller + * @param variables set to the copy of the variables, owned by the caller + * @note The copied variables refer to the promises of the policy, so the + * snapshot must be restored (or destroyed) before the policy is. + */ +void EvalContextSnapshotTake(EvalContext *ctx, ClassTable **classes, VariableTable **variables); + +/** + * @brief Replace the global classes and variables with the snapshot taken by + * EvalContextSnapshotTake(). The persistent classes are not in it, the + * next snapshot loads them again from the state database. + * + * @param classes the copy of the classes, owned by the context afterwards + * @param variables the copy of the variables, owned by the context afterwards + */ +void EvalContextSnapshotRestore(EvalContext *ctx, ClassTable *classes, VariableTable *variables); + void EvalContextOverrideImmutableSet(EvalContext *ctx, bool should_override); bool EvalContextOverrideImmutableGet(EvalContext *ctx); diff --git a/libpromises/variable.c b/libpromises/variable.c index 213c342531a..d0363e15ba0 100644 --- a/libpromises/variable.c +++ b/libpromises/variable.c @@ -257,6 +257,32 @@ bool VariableTablePut(VariableTable *table, const VarRef *ref, return VarMapInsert(table->vars, var->ref, var); } +VariableTable *VariableTableCopy(const VariableTable *table) +{ + assert(table != NULL); + + VariableTable *copy = VariableTableNew(); + + VariableTableIterator *iter = VariableTableIteratorNew(table, NULL, NULL, NULL); + for (const Variable *var = VariableTableIteratorNext(iter); var != NULL; + var = VariableTableIteratorNext(iter)) + { + /* VariableTablePut() copies the value, but takes the tags and the + * comment. The tags may be NULL. */ + StringSet *tags = NULL; + if (var->tags != NULL) + { + tags = StringSetNew(); + StringSetJoin(tags, var->tags, xstrdup); + } + VariableTablePut(copy, var->ref, &var->rval, var->type, tags, + SafeStringDuplicate(var->comment), var->promise); + } + VariableTableIteratorDestroy(iter); + + return copy; +} + bool VariableTableClear(VariableTable *table, const char *ns, const char *scope, const char *lval) { const size_t vars_num = VarMapSize(table->vars); diff --git a/libpromises/variable.h b/libpromises/variable.h index 1c653a85d40..65b779aef32 100644 --- a/libpromises/variable.h +++ b/libpromises/variable.h @@ -73,6 +73,12 @@ bool VariableTableRemove(VariableTable *table, const VarRef *ref); size_t VariableTableCount(const VariableTable *table, const char *ns, const char *scope, const char *lval); bool VariableTableClear(VariableTable *table, const char *ns, const char *scope, const char *lval); +/** + * @brief Deep copy of the table, with all its variables. The copied variables + * refer to the same promises. + */ +VariableTable *VariableTableCopy(const VariableTable *table); + VariableTableIterator *VariableTableIteratorNew(const VariableTable *table, const char *ns, const char *scope, const char *lval); VariableTableIterator *VariableTableIteratorNewFromVarRef(const VariableTable *table, const VarRef *ref); Variable *VariableTableIteratorNext(VariableTableIterator *iter);