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

Refactor and clean up the SyncService #4543

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

poljar
Copy link
Contributor

@poljar poljar commented Jan 16, 2025

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.

@poljar poljar requested a review from a team as a code owner January 16, 2025 16:27
@poljar poljar requested review from andybalaam and removed request for a team January 16, 2025 16:27
@poljar poljar force-pushed the poljar/offline-mode-sync-service-pr branch from 0bc9547 to 5f62220 Compare January 16, 2025 16:38
Copy link
Member

@bnjbvr bnjbvr left a 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.

crates/matrix-sdk-ui/src/sync_service.rs Show resolved Hide resolved
crates/matrix-sdk-ui/src/sync_service.rs Outdated Show resolved Hide resolved
Comment on lines 372 to 448
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
}
}
Copy link
Member

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 :/

Copy link
Contributor Author

@poljar poljar Jan 17, 2025

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. 🎶

Copy link
Member

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 :-)

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

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

⚠️ Actually, your argument doesn't hold. Were we to add a new variant to 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?

Copy link
Contributor Author

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.

f522a81

crates/matrix-sdk-ui/src/sync_service.rs Outdated Show resolved Hide resolved
Copy link

codecov bot commented Jan 16, 2025

Codecov Report

Attention: Patch coverage is 79.56989% with 19 lines in your changes missing coverage. Please review.

Project coverage is 85.43%. Comparing base (1d83d42) to head (f522a81).
Report is 13 commits behind head on main.

Files with missing lines Patch % Lines
crates/matrix-sdk-ui/src/sync_service.rs 79.56% 19 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

Copy link
Member

@andybalaam andybalaam left a 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.

poljar added 10 commits January 22, 2025 16:09
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.
@poljar poljar force-pushed the poljar/offline-mode-sync-service-pr branch from b38e8b9 to dbde96b Compare January 22, 2025 15:17
@poljar poljar enabled auto-merge (rebase) January 22, 2025 15:18
@poljar poljar force-pushed the poljar/offline-mode-sync-service-pr branch from d33383f to e151ea9 Compare January 23, 2025 09:50
Copy link
Member

@bnjbvr bnjbvr left a 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 ⚠️ emoji, otherwise mostly nits here and there. I'd like to take another look before approving. Thanks!

crates/matrix-sdk-ui/src/sync_service.rs Outdated Show resolved Hide resolved
crates/matrix-sdk-ui/src/sync_service.rs Outdated Show resolved Hide resolved
crates/matrix-sdk-ui/src/sync_service.rs Outdated Show resolved Hide resolved
crates/matrix-sdk-ui/src/sync_service.rs Outdated Show resolved Hide resolved
crates/matrix-sdk-ui/src/sync_service.rs Outdated Show resolved Hide resolved
crates/matrix-sdk-ui/src/sync_service.rs Outdated Show resolved Hide resolved
Comment on lines 372 to 448
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
}
}
Copy link
Member

Choose a reason for hiding this comment

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

⚠️ Actually, your argument doesn't hold. Were we to add a new variant to 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?

crates/matrix-sdk-ui/src/sync_service.rs Outdated Show resolved Hide resolved
crates/matrix-sdk-ui/Cargo.toml Show resolved Hide resolved
crates/matrix-sdk-ui/src/sync_service.rs Outdated Show resolved Hide resolved
Copy link
Member

@Hywan Hywan left a 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.

Comment on lines 476 to 483
inner: Arc::new(AsyncMutex::new(SyncServiceInner {
scheduler_task: None,
scheduler_sender: None,
room_list_service,
encryption_sync_service: encryption_sync,
state,
encryption_sync_permit,
})),
Copy link
Member

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.
Copy link
Member

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.

Comment on lines 82 to 86
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>>,
Copy link
Member

Choose a reason for hiding this comment

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

Replacing 2 Options 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);
Copy link
Member

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?

crates/matrix-sdk-ui/src/sync_service.rs Outdated Show resolved Hide resolved
@@ -183,33 +183,38 @@ impl SyncTaskSupervisor {
(room_list_task, encryption_sync_task)
}

fn check_if_expired(err: &matrix_sdk::Error) -> bool {
Copy link
Member

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 😇.

Comment on lines +318 to +322
/// 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.
Copy link
Member

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.

Comment on lines 568 to 569
room_list_service,
encryption_sync_permit,
Copy link
Member

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>,
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

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

- within t
+ within the

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.

4 participants