-
Notifications
You must be signed in to change notification settings - Fork 268
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
Refactor and clean up the SyncService #4543
base: main
Are you sure you want to change the base?
Conversation
0bc9547
to
5f62220
Compare
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.
Haven't looked at all of it, but some of the cosmetic changes seem plain wrong, as they break symmetry with most of the current code style in the UI crate.
match inner.state.get() { | ||
State::Idle | State::Terminated | State::Error => { | ||
// No need to stop if we were not running. | ||
return Ok(()); | ||
Ok(()) | ||
} | ||
State::Running => {} | ||
}; | ||
|
||
trace!("pausing sync service"); | ||
State::Running => { | ||
trace!("pausing sync service"); | ||
|
||
// First, request to stop the two underlying syncs; we'll look at the results | ||
// later, so that we're in a clean state independently of the request to | ||
// stop. | ||
// First, request to stop the two underlying syncs; we'll look at the results | ||
// later, so that we're in a clean state independently of the request to stop. | ||
|
||
// Remove the supervisor from our inner state and request the tasks to be | ||
// shutdown. | ||
let supervisor = inner.supervisor.take().ok_or_else(|| { | ||
error!("The supervisor was not properly started up"); | ||
Error::InternalSupervisorError | ||
})?; | ||
// Remove the supervisor from our inner state and request the tasks to be | ||
// shutdown. | ||
let supervisor = inner.supervisor.take().ok_or_else(|| { | ||
error!("The supervisor was not properly started up"); | ||
Error::InternalSupervisorError | ||
})?; | ||
|
||
supervisor.shutdown().await | ||
supervisor.shutdown().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.
This change feels like a regression: it adds one extra level of indent, for absolutely no extra value. I see the point of doing it in the other function because the body's size is so small there (in particular, there's no control flow), but it's not the case here, so this just adds visual clutter :/
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 body size of the SyncService::start()
and SyncService::stop()
methods was reduced to just a couple of lines in a previous commit, allowing us to do exactly this without introducing visual clutter from indentation.
The value becomes clear when you consider what needs to happen if someone adds another enum variant—something I plan to do.
Consider the examples:
First, the pattern where we flatten out one branch to remove the indentation:
enum Foo {
A,
B,
}
fn bar(foo: Foo) {
match foo {
// Do nothing if Foo::A.
A => return,
B => (),
}
// Do what we would have done in the B branch.
call_something_for_b();
}
Now the way it is in the pull-request:
enum Foo {
A,
B,
}
fn bar(foo: Foo) {
match foo {
// Do nothing if Foo::A.
A => (),
// Do what we need to do for B.
B => {
call_something_for_b();
},
}
}
When we add a new variant, I’ll either need to convert the first variant into the second, or introduce even more early returns, which would make the flow even less clear:
enum Foo {
A,
B,
C,
}
fn bar(foo: Foo) {
match foo {
// Do nothing if Foo::A.
A => return,
B => (),
C => {
// Do what we need for C, can't be flattened out like we did for B.
do_something_for_c();
return;
},
}
// Do what we would have done in the B branch.
call_something_for_b();
}
I hope we can agree that adding more variants would make the flow of execution even more convoluted.
In contrast, the second variant naturally provides a clear place to add a new branch, that's precisely what pattern matching is here for.
enum Foo {
A,
B,
C,
}
fn bar(foo: Foo) {
match foo {
A => (),
B => call_something_for_b(),
C => do_something_for_c(),
}
}
I would urge you to reconsider your stance on this pattern and code style, and to evaluate how flattening the branches of a pattern match might affect the extensibility of the codebase.
Early returns have their, in my opinion, limited use—but this ain't it, babe. 🎶
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.
That you planned to add another variant is something I wasn't aware of, and couldn't guess, so that makes sense in this case 👍
I would urge you to reconsider your stance on this pattern and code style, and to evaluate how flattening the branches of a pattern match might affect the extensibility of the codebase.
No, I'd rather have you reconsider your stance: more early returns make for fewer indent levels, and more "linear" / "simple" / "beautiful" code, where all the assumptions are checked upfront, and then the actual work is done later. Agree to disagree here :-)
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.
That you planned to add another variant is something I wasn't aware of, and couldn't guess, so that makes sense in this case
You seem to be missing the point. Every enum might be expanded to include more variants, the point is to write code that is prepared to easily and exhaustively handle this scenario. The commit did mention this:
Now that the various match branches in the start and stop method of the
SyncService are minimized we can remove the early returns.
This should allow us to more easily add new branches.
No, I'd rather have you reconsider your stance
Well I did, otherwise I would not have rewritten this. I explained the problems such code cause multiple times in this very thread, so could you do your due diligence please as well?
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 can talk about this privately.
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.
State
, the compiler would've complained in the original form of the code that it's not handled in this match, giving us the pause that we're aiming for (to decide how to handle it). So changing the code like you did here is only a regression in terms of readability, and doesn't buy us any type checks that we didn't have before. Can you revert it, please?
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.
That's still not the point I was making.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4543 +/- ##
==========================================
+ Coverage 85.42% 85.43% +0.01%
==========================================
Files 285 285
Lines 32222 32229 +7
==========================================
+ Hits 27525 27536 +11
+ Misses 4697 4693 -4 ☔ 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.
Looks great, thank you for doing this.
Previously we had a lock protecting an empty value, but the logic wants to protect a bunch of data in the SyncService. Let's do the usual thing and create a SyncServiceInner which holds the data and protect that with a lock.
From cambridge a scheduler is defined as: > someone whose job is to create or work with schedules While supervisor is defined as: > a person whose job is to supervise someone or something Well ok, that doesn't tell us much, supervise is defined as: > to watch a person or activity to make certain that everything is done correctly, safely, etc.: In conclusion, supervising a task is the more common and better understood terminology here I would say.
The supervisor is defined as two optional fields that are set and removed at the same time. This patch converts the two optional fields into a single optional struct. The fields inside the struct now aren't anymore optional. This ensures that they are always set and destroyed at the same time.
Now that the various match branches in the start and stop method of the SyncService are minimized we can remove the early returns. This should allow us to more easily add new branches.
b38e8b9
to
dbde96b
Compare
d33383f
to
e151ea9
Compare
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.
I've looked at all the commits now. The commit message for the Scheduler
-> Supervisor
renaming was not really convincing, but the offline discussion where you referred to the computing-specific definition of supervisor
had me agree it's a good name.
My two blocking comments are the ones starting with a
match inner.state.get() { | ||
State::Idle | State::Terminated | State::Error => { | ||
// No need to stop if we were not running. | ||
return Ok(()); | ||
Ok(()) | ||
} | ||
State::Running => {} | ||
}; | ||
|
||
trace!("pausing sync service"); | ||
State::Running => { | ||
trace!("pausing sync service"); | ||
|
||
// First, request to stop the two underlying syncs; we'll look at the results | ||
// later, so that we're in a clean state independently of the request to | ||
// stop. | ||
// First, request to stop the two underlying syncs; we'll look at the results | ||
// later, so that we're in a clean state independently of the request to stop. | ||
|
||
// Remove the supervisor from our inner state and request the tasks to be | ||
// shutdown. | ||
let supervisor = inner.supervisor.take().ok_or_else(|| { | ||
error!("The supervisor was not properly started up"); | ||
Error::InternalSupervisorError | ||
})?; | ||
// Remove the supervisor from our inner state and request the tasks to be | ||
// shutdown. | ||
let supervisor = inner.supervisor.take().ok_or_else(|| { | ||
error!("The supervisor was not properly started up"); | ||
Error::InternalSupervisorError | ||
})?; | ||
|
||
supervisor.shutdown().await | ||
supervisor.shutdown().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.
State
, the compiler would've complained in the original form of the code that it's not handled in this match, giving us the pause that we're aiming for (to decide how to handle it). So changing the code like you did here is only a regression in terms of readability, and doesn't buy us any type checks that we didn't have before. Can you revert it, please?
Termination aligns better with the existing terminology.
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.
I see many improvements in this pull request, thank you for taking time working on this!
- 5 locks replaced by a single one,
- less uninitialized states by replacing 2
Option
(on fields) by a single 1 (on a struct), - less clones,
- a lot more documentation
- and more details.
I've left a couple of comments that you may or may not to address; up to you.
inner: Arc::new(AsyncMutex::new(SyncServiceInner { | ||
scheduler_task: None, | ||
scheduler_sender: None, | ||
room_list_service, | ||
encryption_sync_service: encryption_sync, | ||
state, | ||
encryption_sync_permit, | ||
})), |
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.
Replacing 5 locks by 1 lock, that's what I call an improvement. Well done!
@@ -72,18 +72,18 @@ struct SyncServiceInner { | |||
encryption_sync_service: Arc<EncryptionSyncService>, | |||
state: SharedObservable<State>, | |||
encryption_sync_permit: Arc<AsyncMutex<EncryptionSyncPermit>>, | |||
/// Scheduler task ensuring proper termination. | |||
/// Supervisor task ensuring proper termination. |
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.
I agree it's a better name according to your definitions. Scheduler wasn't “wrong” neither, but I believe Supervisor is more adapted.
supervisor_task: Option<JoinHandle<()>>, | ||
/// A supervisor that starts two sync tasks, one for the room list and one for | ||
/// the end-to-end encryption support. | ||
struct SyncTaskSupervisor { | ||
/// The task that supervises the two sync tasks. | ||
task: JoinHandle<()>, | ||
/// `TerminationReport` sender for the [`Self::stop()`] function. | ||
/// | ||
/// This is set at the same time as all the tasks in [`Self::start()`]. | ||
supervisor_sender: Option<Sender<TerminationReport>>, |
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.
Replacing 2 Option
s by 1 reduces the states in this code, well done. Since it is for the struct itself, and not for its fields, the complexity is also reduced.
receiver: Receiver<TerminationReport>, | ||
) -> Self { | ||
async fn new(inner: &SyncServiceInner) -> Self { | ||
let (sender, receiver) = tokio::sync::mpsc::channel(16); |
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.
Are we sure 16 is enough? Don't we want an unbounded channel, or a channel with a larger buffer?
@@ -183,33 +183,38 @@ impl SyncTaskSupervisor { | |||
(room_list_task, encryption_sync_task) | |||
} | |||
|
|||
fn check_if_expired(err: &matrix_sdk::Error) -> 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.
That's an improvement. It can even be unit tested 😇.
/// A high level manager for your Matrix syncing needs. | ||
/// | ||
/// The [`SyncService`] is responsible for managing real-time synchronization | ||
/// with a Matrix server. It can initiate and maintain the necessary | ||
/// synchronization tasks for you. |
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! That's exactly what we often miss to do in the SDK, this kind of doc is really useful.
room_list_service, | ||
encryption_sync_permit, |
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.
Getting better and better!
@@ -73,7 +73,7 @@ struct SyncTaskSupervisor { | |||
task: JoinHandle<()>, | |||
/// [`TerminationReport`] sender for the [`SyncTaskSupervisor::shutdown()`] | |||
/// function. | |||
abortion_sender: Sender<TerminationReport>, | |||
termination_sender: Sender<TerminationReport>, |
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.
Agree on this.
/// A supervisor responsible for managing two sync tasks: one for handling the | ||
/// room list and another for supporting end-to-end encryption. | ||
/// | ||
/// The two sync tasks are spawned as child tasks and are contained within t |
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.
- within t
+ within the
These are mostly internal changes and don't contain any behavioral change, except for some edge cases that are now a bit better handled.
A review commit by commit should ease the task a bit, but reading the whole file shouldn't be too terrible either, it's not that big.