Skip to content
Open
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
19 changes: 18 additions & 1 deletion libpromises/attributes.c
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,21 @@ ContextConstraint GetContextConstraints(const EvalContext *ctx, const Promise *p
a.expression = NULL;
a.persistent = PromiseGetConstraintAsInt(ctx, "persistence", pp);

{
const char *tp = PromiseGetConstraintAsRval(pp, "timer_policy", RVAL_TYPE_SCALAR);
if (tp != NULL && strcmp(tp, "reset") == 0)
{
a.timer = CONTEXT_STATE_POLICY_RESET;
}
else
{
/* Default to PRESERVE (absolute) for backward compatibility:
* classes: promises historically skip re-evaluation when the
* class is already defined, so the timer was never reset. */
a.timer = CONTEXT_STATE_POLICY_PRESERVE;
}
}

{
const char *context_scope = PromiseGetConstraintAsRval(pp, "scope", RVAL_TYPE_SCALAR);
a.scope = ContextScopeFromString(context_scope);
Expand All @@ -1143,7 +1158,9 @@ ContextConstraint GetContextConstraints(const EvalContext *ctx, const Promise *p

for (int k = 0; CF_CLASSBODY[k].lval != NULL; k++)
{
if (strcmp(cp->lval, "persistence") == 0 || strcmp(cp->lval, "scope") == 0)
if (strcmp(cp->lval, "persistence") == 0 ||
strcmp(cp->lval, "scope") == 0 ||
strcmp(cp->lval, "timer_policy") == 0)
{
continue;
}
Expand Down
1 change: 1 addition & 0 deletions libpromises/cf3.defs.h
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,7 @@ typedef struct
ContextScope scope;
int nconstraints;
int persistent;
PersistentClassPolicy timer;
} ContextConstraint;

/*************************************************************************/
Expand Down
2 changes: 1 addition & 1 deletion libpromises/eval_context.c
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,7 @@ void EvalContextHeapPersistentSave(EvalContext *ctx, const char *name, unsigned

// first see if we have an existing record, and if we should bother to update
{
int existing_info_size = ValueSizeDB(dbp, key, strlen(key));
int existing_info_size = ValueSizeDB(dbp, key, strlen(key) + 1);
if (existing_info_size > 0)
{
PersistentClassInfo *existing_info = xcalloc(existing_info_size, 1);
Expand Down
1 change: 1 addition & 0 deletions libpromises/mod_common.c
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ const ConstraintSyntax CF_CLASSBODY[] =
ConstraintSyntaxNewContext("not", "Evaluate the negation of string expression in normal form", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewContextList("select_class", "Select one of the named list of classes to define based on host identity. Default value: random_selection", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewContextList("xor", "Combine class sources with XOR", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewOption("timer_policy", "absolute,reset", "Whether a persistent class restarts its counter when rediscovered. Default value: absolute", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewNull()
};

Expand Down
25 changes: 20 additions & 5 deletions libpromises/promises.c
Original file line number Diff line number Diff line change
Expand Up @@ -705,16 +705,31 @@ Promise *ExpandDeRefPromise(EvalContext *ctx, const Promise *pp, bool *excluded)
pcopy->org_pp = pp->org_pp;

// if this is a class promise, check if it is already set, if so, skip
// Exception: persistent classes with timer_policy => "reset" must not
// be skipped — the promise needs to fire so the timer gets reset.
if (strcmp("classes", PromiseGetPromiseType(pp)) == 0)
{
if (IsDefinedClass(ctx, CanonifyName(pcopy->promiser)))
{
Log(LOG_LEVEL_DEBUG,
"Skipping evaluation of classes promise as class '%s' is already set",
CanonifyName(pcopy->promiser));
const char *tp = PromiseGetConstraintAsRval(pp, "timer_policy", RVAL_TYPE_SCALAR);
int persistence = PromiseGetConstraintAsInt(ctx, "persistence", pp);

*excluded = true;
return pcopy;
if (tp != NULL && strcmp(tp, "reset") == 0 && persistence > 0)
{
Log(LOG_LEVEL_DEBUG,
"Class '%s' is already set but timer_policy is reset"
" — allowing promise evaluation to reset persistence timer",
CanonifyName(pcopy->promiser));
}
else
{
Log(LOG_LEVEL_DEBUG,
"Skipping evaluation of classes promise as class '%s' is already set",
CanonifyName(pcopy->promiser));

*excluded = true;
return pcopy;
}
}
}

Expand Down
35 changes: 32 additions & 3 deletions libpromises/verify_classes.c
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,37 @@ PromiseResult VerifyClassPromise(EvalContext *ctx, const Promise *pp, ARG_UNUSED
return PROMISE_RESULT_FAIL;
}

if (a.context.expression == NULL ||
EvalClassExpression(ctx, a.context.expression, pp))
bool class_expression_true =
(a.context.expression == NULL ||
EvalClassExpression(ctx, a.context.expression, pp));

/* When a persistent class is already defined (loaded from DB),
* EvalClassExpression short-circuits and returns false. If the
* timer_policy is "reset", we still need to reset the persistence
* timer in the DB even though the class is already in the context. */
if (!class_expression_true &&
a.context.persistent > 0 &&
a.context.timer == CONTEXT_STATE_POLICY_RESET &&
IsDefinedClass(ctx, pp->promiser))
{
StringSet *tags = StringSetNew();
StringSetAdd(tags, xstrdup("source=promise"));
for (const Rlist *rp = PromiseGetConstraintAsList(ctx, "meta", pp); rp; rp = rp->next)
{
StringSetAdd(tags, xstrdup(RlistScalarValue(rp)));
}
Log(LOG_LEVEL_VERBOSE,
"C: + Resetting persistent class timer: '%s' (%d minutes)",
pp->promiser, a.context.persistent);
Buffer *buf = StringSetToBuffer(tags, ',');
EvalContextHeapPersistentSave(ctx, pp->promiser, a.context.persistent,
CONTEXT_STATE_POLICY_RESET, BufferData(buf));
BufferDestroy(buf);
StringSetDestroy(tags);
return PROMISE_RESULT_NOOP;
}

if (class_expression_true)
{
if (a.context.expression == NULL)
{
Expand Down Expand Up @@ -131,7 +160,7 @@ PromiseResult VerifyClassPromise(EvalContext *ctx, const Promise *pp, ARG_UNUSED
pp->promiser, a.context.persistent);
Buffer *buf = StringSetToBuffer(tags, ',');
EvalContextHeapPersistentSave(ctx, pp->promiser, a.context.persistent,
CONTEXT_STATE_POLICY_RESET, BufferData(buf));
a.context.timer, BufferData(buf));
BufferDestroy(buf);
}
if (inserted && (comment != NULL))
Expand Down
62 changes: 62 additions & 0 deletions tests/acceptance/02_classes/01_basic/persistent_timer_policy.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#######################################################
#
# CFE-4681: classes: promises should support timer_policy
#
# Verify that timer_policy => "absolute" on a classes: promise
# is parsed correctly and passed to EvalContextHeapPersistentSave
# as CONTEXT_STATE_POLICY_PRESERVE. The verbose log message
# should contain "policy preserve".
#
#######################################################

body common control
{
inputs => { "../../default.sub.cf" };
bundlesequence => { default("$(this.promise_filename)") };
version => "1.0";
}

bundle agent init
{
# Remove the persistent class DB to ensure a clean state.
files:
"$(sys.workdir)/state/cf_state.lmdb"
delete => tidy;
"$(sys.workdir)/state/cf_state.lmdb-lock"
delete => tidy;
"$(sys.workdir)/state/cf_state.lmdb.lock"
delete => tidy;
}

bundle agent test
{
meta:
"description" -> { "CFE-4681" }
string => "timer_policy => absolute on classes: promises stores CONTEXT_STATE_POLICY_PRESERVE";

commands:
# Run sub-policy that defines a persistent class with
# timer_policy => "absolute"
"$(sys.cf_agent) -Kv -f $(this.promise_filename).sub > $(G.testdir)/timer_policy_run1.log 2>&1"
contain => in_shell,
classes => always("done");
}

bundle agent check
{
classes:
done::
# Verify the log contains "policy preserve" (not "policy reset")
"ok" expression => regline(".*Creating persistent class.*timer_policy_test_class.*policy preserve.*",
"$(G.testdir)/timer_policy_run1.log");

reports:
DEBUG.done.!ok::
"FAIL: log did not contain 'policy preserve' for timer_policy_test_class";
!done::
"$(this.promise_filename) FAIL (sub-agent run did not complete)";
done.ok::
"$(this.promise_filename) Pass";
done.!ok::
"$(this.promise_filename) FAIL";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
body common control
{
bundlesequence => { run };
}

bundle agent run
{
classes:
# Define persistent class with timer_policy => "absolute"
# This stores CONTEXT_STATE_POLICY_PRESERVE in the DB
"timer_policy_test_class"
expression => "any",
persistence => "120",
timer_policy => "absolute";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#######################################################
#
# CFE-4681: classes: promises with timer_policy => "reset"
#
# Verify that timer_policy => "reset" on a classes: promise
# causes the persistence timer to be reset on subsequent
# agent runs, even though the class is already defined
# (loaded from the persistent DB).
#
# First run: expect "Creating persistent class"
# Second run: expect "Resetting persistent class" (not skipped)
#
#######################################################

body common control
{
inputs => { "../../default.sub.cf" };
bundlesequence => { default("$(this.promise_filename)") };
version => "1.0";
}

bundle agent init
{
# Remove the persistent class DB to ensure a clean state.
files:
"$(sys.workdir)/state/cf_state.lmdb"
delete => tidy;
"$(sys.workdir)/state/cf_state.lmdb-lock"
delete => tidy;
"$(sys.workdir)/state/cf_state.lmdb.lock"
delete => tidy;
}

bundle agent test
{
meta:
"description" -> { "CFE-4681" }
string => "timer_policy => reset on classes: promises resets the persistence timer on subsequent runs";

commands:
# First run: define the persistent class
"$(sys.cf_agent) -Kv -f $(this.promise_filename).sub > $(G.testdir)/timer_reset_run1.log 2>&1"
contain => in_shell,
classes => always("first_done");
}

bundle agent check
{
commands:
first_done::
# Second run: class already exists in DB, timer_policy=reset
# should cause the timer to be reset
"$(sys.cf_agent) -Kv -f $(this.promise_filename).sub > $(G.testdir)/timer_reset_run2.log 2>&1"
contain => in_shell,
classes => always("second_done");

classes:
second_done::
"first_ok" expression => regline(".*Creating persistent class.*timer_reset_test_class.*",
"$(G.testdir)/timer_reset_run1.log");
"second_ok" expression => regline(".*Resetting persistent class.*timer_reset_test_class.*",
"$(G.testdir)/timer_reset_run2.log");
"ok" and => { "first_ok", "second_ok" };

reports:
DEBUG.second_done.!first_ok::
"FAIL: first run did not log 'Creating persistent class'";
DEBUG.second_done.!second_ok::
"FAIL: second run did not log 'Resetting persistent class' (short-circuit not bypassed)";
!first_done::
"$(this.promise_filename) FAIL (first sub-agent run did not complete)";
first_done.!second_done::
"$(this.promise_filename) FAIL (second sub-agent run did not complete)";
second_done.ok::
"$(this.promise_filename) Pass";
second_done.!ok::
"$(this.promise_filename) FAIL";
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
body common control
{
bundlesequence => { run };
}

bundle agent run
{
classes:
# Define persistent class with timer_policy => "reset"
# On second run, the timer should be reset even though
# the class is already defined from the persistent DB.
"timer_reset_test_class"
expression => "any",
persistence => "120",
timer_policy => "reset";
}
46 changes: 46 additions & 0 deletions tests/unit/eval_context_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,51 @@ void test_eval_with_token_from_list(void)
StringSetDestroy(time_classes);
}

static void test_persistent_class_timer_policy(void)
{
EvalContext *ctx = EvalContextNew();

/* Save a persistent class with PRESERVE policy, 60 minute TTL */
EvalContextHeapPersistentSave(ctx, "timer_test", 60,
CONTEXT_STATE_POLICY_PRESERVE, "tag1");

/* Verify the class loads correctly after PRESERVE save */
EvalContextHeapPersistentLoadAll(ctx);

{
const Class *cls = EvalContextClassGet(ctx, "default", "timer_test");
assert_true(cls != NULL);
assert_string_equal("timer_test", cls->name);
}

/* Save again with PRESERVE -- the function should early-return
* (class is preserved, not expired, same tags), leaving the DB
* record unchanged. We verify by loading persistent classes and
* checking the class is still defined. */
EvalContextHeapPersistentSave(ctx, "timer_test", 60,
CONTEXT_STATE_POLICY_PRESERVE, "tag1");

/* Class should still be defined after the second PRESERVE save */
{
const Class *cls = EvalContextClassGet(ctx, "default", "timer_test");
assert_true(cls != NULL);
assert_string_equal("timer_test", cls->name);
}

/* Save with RESET policy -- the record SHOULD be overwritten.
* The class should still be loadable afterward. */
EvalContextHeapPersistentSave(ctx, "timer_test", 60,
CONTEXT_STATE_POLICY_RESET, "tag1");

{
const Class *cls = EvalContextClassGet(ctx, "default", "timer_test");
assert_true(cls != NULL);
assert_string_equal("timer_test", cls->name);
}

EvalContextDestroy(ctx);
}

int main()
{
PRINT_TEST_BANNER();
Expand All @@ -160,6 +205,7 @@ int main()
const UnitTest tests[] =
{
unit_test(test_class_persistence),
unit_test(test_persistent_class_timer_policy),
unit_test(test_changes_chroot),
unit_test(test_eval_with_token_from_list),
};
Expand Down
Loading