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

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

@@ -67,48 +67,40 @@ pub enum State {
Error,
}

pub struct SyncService {
/// Room list service used to synchronize the rooms state.
Copy link
Member

Choose a reason for hiding this comment

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

Why did you remove all the fields comments? :/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, one of them was lost in the move. Or do you mean all those should be replicated in the Inner struct?

Anyways, I added the missing one back: 9ae0235.

crates/matrix-sdk-ui/src/sync_service.rs Outdated Show resolved Hide resolved
Comment on lines 372 to 392
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 :-)

Comment on lines 227 to 228
let report =
TerminationReport { is_error, has_expired, origin: TerminationOrigin::EncryptionSync };
Copy link
Member

Choose a reason for hiding this comment

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

I don't see the point of the report variables: they only add one level of "indirection" (some extra thing to keep in mind for a reader), they create one extra binding for the compiler frontend to handle, and they're used only once, so might as well be kept inlined at use.

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 prefer the formatting of the two statements compared to the formatting of the single statement, but ok: b38e8b9.

Copy link

codecov bot commented Jan 16, 2025

Codecov Report

Attention: Patch coverage is 77.52809% with 20 lines in your changes missing coverage. Please review.

Project coverage is 85.42%. Comparing base (c24770a) to head (b38e8b9).
Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
crates/matrix-sdk-ui/src/sync_service.rs 77.52% 20 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4543      +/-   ##
==========================================
+ Coverage   85.40%   85.42%   +0.01%     
==========================================
  Files         285      285              
  Lines       32213    32213              
==========================================
+ Hits        27511    27517       +6     
+ Misses       4702     4696       -6     

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

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