-
Notifications
You must be signed in to change notification settings - Fork 252
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
sdk: Allow to update notification settings #2135
Conversation
Codecov ReportPatch coverage:
Additional details and impacted files@@ Coverage Diff @@
## main #2135 +/- ##
==========================================
+ Coverage 76.11% 76.25% +0.13%
==========================================
Files 161 164 +3
Lines 17202 17330 +128
==========================================
+ Hits 13094 13215 +121
- Misses 4108 4115 +7
☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! There's a potential data race issue in there, and we ought to simplify the insert_rule
function. This is close to merge 💪
kind: RuleKind, | ||
room_id: &RoomId, | ||
notify: bool, | ||
delete_other_custom_rules: bool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear to me why it's this function's responsibility to delete other custom rules: it has nothing to do with the name, and it seems the caller has the possibility of calling another method, delete_rules
, to do just that. Could we let the caller handle that, instead? It would make for a smaller API surface, fewer things to test, etc.
(The caller can even do better: 1. first remove all the custom rules, if needs be, 2. insert the new rule afterwards; that would not require the exception in the delete_rules
call)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yes, good point. It makes things much simpler, we just have to keep executing the deletions after the insertions (otherwise the mode won' be correct on the next sync)
/// Replace the internal ruleset | ||
/// | ||
/// # Arguments | ||
/// | ||
/// * `ruleset` - A `Ruleset` containing account's owner push rules | ||
async fn set_ruleset(&self, ruleset: &Ruleset) { | ||
*self.rules.write().await = Rules::new(ruleset.clone()) | ||
} | ||
|
||
/// Get a new `Rules` instance to interact with the ruleset. | ||
async fn rules(&self) -> Rules { | ||
let rules = &*self.rules.read().await; | ||
rules.clone() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two methods hide the fact that it's hidden behind a lock, and they may cause confusing issues, looking at how they're used:
- get the ruleset
- do something with it locally, start requests to the server
- replace the internal ruleset
Since only operations 1 and 3 are holding onto the lock, there might be other operations holding onto the lock in the middle. Namely, a new PushRulesEvent
may arrive and reset the ruleset (invalidating it in the meanwhile), or another independent request might be interleaved.
Depending on the answer to the above question (are our requests validated by receiving a new PushRulesEvent
), maybe we don't even need to write to the local push rules. Otherwise, for operations that do read/request/writes, we need another solution:
- taking the write lock throughout the entire request would be a bit sad, but safe
- we could get the read-only
Rules
, then send the request, and after it's done apply a "diff" corresponding to theCommand
to the localRules
(or, equivalently, rerun the localRules
functions on theself.rules.write().await
after the server has responded) - I wonder if the server responds with a partial/updated list of push rules? In that case, we could consider the server's response is the one source of truth
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so as requested, I modified the following:
- I removed the 2 functions that were hiding the lock.
- I modified the flow used to update the ruleset. Now, every time we want to update the ruleset, we first build a list of commands that reflect the change, then we execute them to update the HS and finally, we update the local ruleset by applying the commands on the current ruleset. Applying a command can fail silently if the current ruleset has changed between the time we build the list of commands and the time we apply them to the ruleset.
} | ||
|
||
/// Get a new `Rules` instance to interact with the ruleset. | ||
async fn rules(&self) -> Rules { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this return a RwLockReadGuard<'_, Rules>
, instead? So it's the caller's responsibility to decide whether to clone or not, depending on its needs (I suspect most of the time it's not required to clone).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good point. I've updated it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're getting there!
pub(crate) fn insert_room_rule( | ||
&mut self, | ||
/// Build a command to insert a push rule | ||
pub(crate) fn build_insert_rule_command( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so first I thought all the _command
methods don't really use self
(directly or indirectly), and that suggests a slightly different code structuring here.
But when making a delete command, we shouldn't just let pass through an error where we're trying to remove a rule that doesn't exist. So it could be useful to keep a look at the current rules.
So we can go one of two ways:
- Either rename all these methods
prepare_{action}_command
, but have them do real validation. - Or having a new
struct RuleCommands<'a> { commands: Vec<Command>, rules: &'a Ruleset }
that has many helper methodsinsert_rule
, etc., and it's independent from theRules
? Then we change the signature ofRules::apply
so it takes aRuleCommands
parameter. Commands can be tested separately that way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I've chosen option 2, the RuleCommands
struct simplifies the insertion / deletion of the rules.
/// | ||
/// The commands may silently fail because the ruleset may have changed | ||
/// between the time the commands were created and the time it is applied. | ||
pub(crate) fn apply_commands(&mut self, commands: &[Command]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should take ownership of the Vec<Command>
, unless we have a (non-test only) reason to reuse them after applying.
room_id: &RoomId, | ||
mode: RoomNotificationMode, | ||
) -> Result<(), NotificationSettingsError> { | ||
let rules = self.rules.read().await.clone(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
let custom_rules = self.get_custom_rules_for_room(room_id).await; | ||
|
||
let rules = self.rules.read().await.clone(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The read lock is implicitly taken in get_custom_rules_for_room
, could we take it only once? (inlining the content of get_custom_rules_for_room
here)
(also make sure to release it before the execute_commands
below, to avoid locking for too long!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I've updated the code so that it waits for a lock as little as possible.
let mut rules = notification_settings.rules.read().await.clone(); | ||
_ = insert_room_rule(&mut rules, RuleKind::Room, &room_id, true).unwrap(); | ||
update_rules(¬ification_settings, &rules).await; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of this pattern, could we statically declare the rules manually in a Vec
and create a NotificationSettings
from that, without using other orthogonal features? Otherwise, we lose a bit of control over the things we test and thus we might be using broken code (in insert_room_rule
and others) while not realizing it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
let mut rules = notification_settings.rules.read().await.clone(); | ||
// Set a notifying `Room` rule into the ruleset to be in `AllMessages` | ||
_ = insert_room_rule(&mut rules, RuleKind::Room, &room_id, true).unwrap(); | ||
update_rules(¬ification_settings, &rules).await; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto everywhere in this file
…notify actions; use clone() where it's more obvious and use explicit variants so we know we have to consider this match if we were to add a new variant later
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. So as to help with merging this, I'll push my own review comments as commits to this branch, and you can inspect them to see what I've changed. In particular, there are a few review comments inline as XXX
comments, which I think deserve clarifications as follow-up PRs (which should either change the code, or at least remove the XXX
comments 😁).
This PR is the second step of the #1959 implementation.
It adds a new
notification_settings
crate in matrix-sdk to get and define notification settings for a room and enable/disable push rules.The
pub
functions will be used by the ffi layer in a future PR.