Skip to content

docs: add update scheduling policy and clarify action scheduling semantics#327

Open
lsk567 wants to merge 3 commits intomainfrom
docs/action-scheduling-policies
Open

docs: add update scheduling policy and clarify action scheduling semantics#327
lsk567 wants to merge 3 commits intomainfrom
docs/action-scheduling-policies

Conversation

@lsk567
Copy link
Contributor

@lsk567 lsk567 commented Feb 19, 2026

Summary

  • Added documentation for the "update" scheduling policy (the 4th policy alongside drop, defer, replace)
  • Clarified the semantic distinction between defer/drop (handle new events relative to all events) vs replace/update (modify existing pending events only)
  • Documented that replace and update do NOT guarantee min_spacing when earlier event is already committed
  • Added watchdog timer pattern example using logical action election_timeout_reached(0 sec, forever, "update") for Raft consensus
  • Added test references: test/C/src/LastTimeUpdate.lf and test/C/src/concurrent/AsyncCallbackUpdate.lf

Context: lf-lang/reactor-uc#334

Changes

  • docs/writing-reactors/actions.mdx (+31/-1 lines)

…ntics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lsk567 lsk567 requested a review from edwardalee February 19, 2026 01:20
Copy link
Contributor

@edwardalee edwardalee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incredibly subtle. It feels like we need a lawyer to write the docs! I think I found a contradiction in the docs, however, so it still needs refinement.

If a `<min_spacing>` has been declared, then it gives a minimum logical time
interval between the tags of two subsequently scheduled events. The first effect this
has is that events will have monotically increasing tags. The difference between the
times of two successive tags is at least `<min_spacing>`. If the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
times of two successive tags is at least `<min_spacing>`, with exceptions noted below. If the

@@ -202,9 +202,35 @@
- `"defer"`: (**the default**) The event is added to the event queue with a tag that is equal to earliest time that satisfies the minimal spacing requirement. Assuming the time of the preceding event is _t_prev_, then the tag of the new event simply becomes _t_prev_ + `<min_spacing>`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest adding:

A `min_spacing` of 0 with a `"defer"` policy is the same as having no `min_spacing` declaration at all.

- `"defer"`: (**the default**) The event is added to the event queue with a tag that is equal to earliest time that satisfies the minimal spacing requirement. Assuming the time of the preceding event is _t_prev_, then the tag of the new event simply becomes _t_prev_ + `<min_spacing>`.
- `"drop"`: The new event is dropped and `schedule()` returns without having modified the event queue.
- `"replace"`: The payload (if any) of the new event is assigned to the preceding event if it is still pending in the event queue; no new event is added to the event queue in this case. If the preceding event has already been pulled from the event queue, the default `"defer"` policy is applied.
- `"update"`: When a new event _e'_ is scheduled at time _t'_, the scheduler checks two conditions: (1) whether the event queue contains an earlier pending event _e_ at time _t_ for the same action, and (2) whether _t'_ >= _t_ + `<min_spacing>`. If **both** conditions are true, the earlier event _e_ at _t_ is dropped and the new event _e'_ at _t'_ is kept. If **either** condition is false, the new event _e'_ at _t'_ is scheduled normally.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `"update"`: When a new event _e'_ is scheduled at time _t'_, the scheduler checks two conditions: (1) whether the event queue contains an earlier pending event _e_ at time _t_ for the same action, and (2) whether _t'_ >= _t_ + `<min_spacing>`. If **both** conditions are true, the earlier event _e_ at _t_ is dropped and the new event _e'_ at _t'_ is kept. If **either** condition is false, the new event _e'_ at _t'_ is scheduled normally.
- `"update"`: When a new event _e'_ is scheduled at time _t'_, the scheduler checks two conditions: (1) whether the event queue contains an earlier pending event _e_ at some time _t_ for the same action, and (2) whether _t'_ >= _t_ + `<min_spacing>`. If **both** conditions are true, the earlier event _e_ at _t_ is dropped and the new event _e'_ at _t'_ is kept. If **either** condition is false, the new event _e'_ at _t'_ is scheduled normally, as if no `min_spacing` had been specified.


**Semantic distinction between policies.** The four policies fall into two categories:

- **"defer" and "drop"** govern how a _new_ event is handled relative to _all_ previously scheduled events for the same action, including events that have already been committed (i.e., already popped from the event queue and assigned to a tag). These policies decide whether the new event should be added to the queue or not.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- **"defer" and "drop"** govern how a _new_ event is handled relative to _all_ previously scheduled events for the same action, including events that have already been committed (i.e., already popped from the event queue and assigned to a tag). These policies decide whether the new event should be added to the queue or not.
- **"defer" and "drop"** govern how a _new_ event is handled relative to the most recent previously scheduled event for the same action, including events that have already been committed (i.e., already popped from the event queue and processed). These policies decide whether the new event should be added to the queue or not.

@@ -202,9 +202,35 @@
- `"defer"`: (**the default**) The event is added to the event queue with a tag that is equal to earliest time that satisfies the minimal spacing requirement. Assuming the time of the preceding event is _t_prev_, then the tag of the new event simply becomes _t_prev_ + `<min_spacing>`.
- `"drop"`: The new event is dropped and `schedule()` returns without having modified the event queue.
- `"replace"`: The payload (if any) of the new event is assigned to the preceding event if it is still pending in the event queue; no new event is added to the event queue in this case. If the preceding event has already been pulled from the event queue, the default `"defer"` policy is applied.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last sentence here implies that "replace" does guarantee that min_spacing is respected. Is this what the current implementation actually does? In any case, this sentence contradicts what is said below.

- `"update"` keeps the _newer_ event's time and drops the earlier pending event.

:::caution
The `"replace"` and `"update"` policies do **not** guarantee that `<min_spacing>` is always respected. If the earlier event has already been popped from the event queue (i.e., the runtime has already committed to executing at that tag), then the policy cannot find it to replace or update. In that case, the new event is scheduled normally, which may result in two events closer together than `<min_spacing>`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This contradicts the description of "replace" above.

logical action election_timeout_reached(0 sec, forever, "update")
```

Here, the `forever` min_spacing ensures that at most one event for this action is on the event queue at any time (since no two events can ever satisfy a spacing of `forever`). Each time a heartbeat is received, the reactor schedules a new `election_timeout_reached` event in the future. Because the policy is `"update"`, the new event replaces the old pending timeout event, effectively resetting the watchdog. If no heartbeat arrives before the timeout, the pending event fires and triggers an election.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Here, the `forever` min_spacing ensures that at most one event for this action is on the event queue at any time (since no two events can ever satisfy a spacing of `forever`). Each time a heartbeat is received, the reactor schedules a new `election_timeout_reached` event in the future. Because the policy is `"update"`, the new event replaces the old pending timeout event, effectively resetting the watchdog. If no heartbeat arrives before the timeout, the pending event fires and triggers an election.
Here, the `forever` min_spacing ensures that at most one event for this action is on the event queue at any time (since no two events can ever satisfy a spacing of `forever`). Each time a heartbeat is received, the reactor schedules a new `election_timeout_reached` event in the future. Because the policy is `"update"`, the new event replaces the old pending timeout event, effectively resetting the watchdog. If no heartbeat arrives before the timeout, the pending event fires and triggers a reaction, which then handles the timeout, for example by triggering an election.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants