Skip to content
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

feat: extended counter updates and add signalling. #30

Merged
merged 8 commits into from
Sep 9, 2024
Merged

feat: extended counter updates and add signalling. #30

merged 8 commits into from
Sep 9, 2024

Conversation

pylls
Copy link
Contributor

@pylls pylls commented Sep 4, 2024

  • Added support for internal signalling between machines with Event::Signal.
  • It is now possible to update both counters on state transition.
  • A counter operation can now use the value of the other counter.
  • Updated changelog, also with minor clarifications.

@faern and @ewitwer , please have a look. I squashed the history.

ewitwer and others added 2 commits September 4, 2024 16:47
- Added support for internal signaling between machines with
  Event::Signal.
- It is now possible to update both counters on state transition.
- A counter operation can now use the value of the other counter.
- Updated changelog, also with minor clarifications.
Copy link
Collaborator

@faern faern left a comment

Choose a reason for hiding this comment

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

Nice! I left some comments about things that I think can be improved slightly. It's mostly nitpicks. But it would be more idiomatic, and potentially avoid panics (if future changes are not very careful)

In general it's a good rule of thumb to avoid unwraps as much as possible. It's almost always possible to access values in other, panic free, ways.

crates/maybenot/src/constants.rs Show resolved Hide resolved
crates/maybenot/src/constants.rs Show resolved Hide resolved
match self.dist {
None => 1,
Some(dist) => {
let s = dist.sample(rng) as u64;
s.min(MAX_SAMPLED_COUNTER_VALUE)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will not yield all values in the range 0 - MAX_SAMPLED_COUNTER_VALUE. But I guess you know that. This part of the machine is not something I know very much about.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess you're referring to the rounding down of the int part of f64 + size difference? Ended up cutting the constant now, the converting from samples f64 to u64 is fine for all possible use-cases of the framework right now.

crates/maybenot/src/framework.rs Outdated Show resolved Hide resolved
self.transition(excluded, Event::Signal);
}
}
self.signal_pending = None;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of having to manually clear the signal, you can use Option::take to get the value and at the same time set it to None. if let Some(signal) = self.signal_pending.take() {...}

crates/maybenot/src/framework.rs Outdated Show resolved Hide resolved
}

if let Some(excluded) = excluded {
if self.signal_pending.unwrap().is_none() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

You already have access to the signal in the variable signal here. So instead of this, would not just signal.is_none() work? If you do use my Option::take recommendation in the other comment, this would not work any longer, because the unwrap would panic since the value is already taken out of the pending signal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I made the same comment to Ethan at first that we already have access to the signal, but it turns out that the signal is copied there (I think?), and when signal_pending is set again in transition, we need to check for it again.

Regardless, I refactored this part, and it looks clearer now. Please let me know if you agree.

Copy link
Collaborator

Choose a reason for hiding this comment

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

That was indeed the issue - without accessing signal_pending directly, we didn't get the updated signal. The refactor also solves that problem, though, and I think it makes things look cleaner.

@pylls
Copy link
Contributor Author

pylls commented Sep 5, 2024

I pushed another related fix here after a remark by Ethan: now that we can set two counters in the same operation, it was possible to create a machine with an infinite loop of CounterZero events. This fix puts a limit on one CounterZero event per process_event and counter. I'm not sure this is the ideal fix from a machine developer's point of view, but it's guaranteed safe at least.

Looking over the other events triggered internally for loops, I reason as follows:

  • Signal cannot loop because we enforce one signal per event per machine in trigger_events
  • LimitReached happens internally due to decrement_limit, which in turn is only due to external events

Did I miss something?

@ewitwer
Copy link
Collaborator

ewitwer commented Sep 7, 2024

I'm happy with that solution for avoiding CounterZero loops. From my early experiences with the new v2 features, I think that machine developers will be able to live with it; in fact, it might not be an issue at all (triggering more than two CounterZero events in a row doesn't seem to be particularly useful).

Regarding the other events that might be able to loop, I reason in the same way - there seems to be no chance of looping if we consider them in isolation. The thing I try to be most careful about is creating the possibility of a loop when combining, e.g., signals and counters. I also don't think this is a problem, though, because we enforce the restrictions at a fairly high level in trigger_events.

@pylls
Copy link
Contributor Author

pylls commented Sep 9, 2024

Thanks @ewitwer ! Then we're good from a machine developer's perspective feature-wise.

Hmm, when combining events, it should be possible to do something like LimitReached -> Signal -> CounterZero -> CounterZero.

The most computationally intense action is BlockOutgoing because it can cause three samplings from distributions. Updating each counter on a state transition is another two samplings. We also have to remember the original event triggering the chain, so five transitions in total. Each transition also uses distribution sampling due to probabilistic transitions.

That means an adversarially crafted machine can cause 30 samplings from distributions per event if no event batching occurs.

When batching, we now limit the number of CounterZero per call to trigger_events() to two. Limits can only be decremented due to performed actions (PaddingSent, BlockingBegin, and TimerBegin) transitioning to the same state (no further limit sampling), so per definition, they will not occur more than once per machine per batch.

The outlier for batching becomes Signal: it's potentially happening in every event, even when batching. @ewitwer , how about we move signalling backoutside of the events loop in trigger_events(), as you suggested in the initial PR? It'll be something machine developer's have to take into account, but it'd be safer for integrators.

Edit: pushed how this would look like below.

Singalling now happens at most once per machine and per trigger_events,
instead of potentially once per machine and event.

Improves safety for an integrator when batching due to load (by better
reducing total computation) at the cost of machine developer's
convenience.
@ewitwer
Copy link
Collaborator

ewitwer commented Sep 9, 2024

Works for me. It is probably a better trade-off, at least for some integrations, to restrict signals on a per-batch basis like we do for counters. Semantically, I think it still makes sense sense to do it this way, even if it makes designing machines trickier.

@pylls
Copy link
Contributor Author

pylls commented Sep 9, 2024

Great, that's a wrap for this PR. After this, will close and merge it, followed by some 2.0 work as planned.

Just for future reference, on the topic of looping machines and concerns around adversarial machines: One blunt but effective solution is to add a transition budget parameter to process_event and/or the top trigger_events, where each call to transition decrements and stops after hitting zero. This allows an integrator to get tight guarantees about execution time while also allowing machines to grow even more complex (should we want to for v3 or onwards).

@pylls pylls merged commit c00c9ce into v2 Sep 9, 2024
7 checks passed
@pylls pylls deleted the v2-new branch September 9, 2024 17:45
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.

3 participants