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] Concurrency improvements #52

Open
wants to merge 45 commits into
base: dev
Choose a base branch
from

Conversation

matejmolnar
Copy link
Collaborator

@matejmolnar matejmolnar commented Nov 27, 2023

This PR aims to solve this issue #50

Changes:

  • Set the strict swift concurrency build setting to complete for both the ios-networking package and for the NetworkingSampleApp.
  • Create a global actor @NetworkingActor where all networking related operations should synchronize.
  • Conform a bunch of protocols/classes to @NetworkingActor
  • Conform a bunch of types to Sendable
  • Remove ThreadSafeDictionary and replace with regular dictionaries since we are processing all operations on @NetworkingActor.
  • Some warnings regarding non-sendable types being used within isolated context had to be silenced by adding @preconcurrency keyword before import of a given module causing the issues (mainly Combine), with a comment explaining why it had to be done.
  • The biggest changes with which I've struggled the most are in MultipeerConnectivityManager and EndpointRequestStorageProcessor, mainly due to the fact that we want to get a device name by calling UIDevice.current.name which from now on has to be done on MainActor. As you can see in the code it's quite tricky to inject a MainActor parameter into another class/actor that is not a MainActor, but it can be done, even though it's not elegant at all.

@matejmolnar matejmolnar changed the title Feat/concurrency improvements [WIP] Feat/concurrency improvements Nov 27, 2023
@matejmolnar matejmolnar requested a review from a team December 4, 2023 18:29
@matejmolnar matejmolnar changed the title [WIP] Feat/concurrency improvements [feat] Concurrency improvements Dec 4, 2023
@matejmolnar matejmolnar linked an issue Dec 6, 2023 that may be closed by this pull request
Copy link
Contributor

@johnkodes johnkodes left a comment

Choose a reason for hiding this comment

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

@matejmolnar thank you for the changes 🙏

Before I'll finish the whole code review, I would like to hear some reasoning behind the changes.

I'm by no means an expert on Complete Concurrency so take the comments also as me trying to get more context.

My comments were mainly driven by how can we keep the library customizable (eg. inheritable) but thread-safe at the same time.

I wonder if we should also think about creating a global actor that will be used in the library to make the actor/thread synchronization easier.

Also, could we also maybe try adding some tests that verify thread-safety? ☺️

@@ -8,7 +8,7 @@
import Networking
import Foundation

final class SampleAuthorizationManager: AuthorizationManaging {
actor SampleAuthorizationManager: AuthorizationManaging {
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the different of using this final class SampleAuthorizationManager: AuthorizationManaging, Sendable versus using an actor? 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This warning Stored property 'apiManager' of 'Sendable'-conforming class 'SampleAuthorizationManager' has non-sendable type 'APIManager'

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, this change would count with making the APIManager unchecked Sendable. Is there anything that's stopping from doing that? Everything is a let and then we have the actor for the counter. Or am I missing something? 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

But I would love us to first write unit tests that will check thread safety of the APIManager, DownloadAPIManager etc., used as an actor and then verify that we actually need it and if not, we would safely mark it as unchecked Sendable?

@matejmolnar
Copy link
Collaborator Author

@johnkodes Ok I think I've finally found the proper solution to basically all the issues we were discussing 🤞😅. I've created a single global actor @NetworkingActor and conformed all the necessary protocols/classes to it. What do you think?

matejmolnar and others added 2 commits January 2, 2024 17:22
Co-authored-by: Jan Kodeš <jan.kodes@strv.com>
Co-authored-by: Jan Kodeš <jan.kodes@strv.com>
@johnkodes
Copy link
Contributor

@johnkodes Ok I think I've finally found the proper solution to basically all the issues we were discussing 🤞😅. I've created a single global actor @NetworkingActor and conformed all the necessary protocols/classes to it. What do you think?

@matejmolnar did you find some information on whether using a single Global Actor for everything could lead to performance drawbacks? 🤔 Eg. by marking everything NetworkingActor we will have everything act as a serial queue right?

@matejmolnar
Copy link
Collaborator Author

@johnkodes Good point. I tried researching this topic and to be honest I am a little confused. I think it should be ok, but I am not 100% sure.
regarding this statement by marking everything NetworkingActor we will have everything act as a serial queue . Are you are worried that running parallel requests won't work? Because that's not the case. You can even try the parallel requests in the users sample. You can see in the logs that the requests fire at once and after a while the responses arrive in a random order.

Copy link
Collaborator

@cejanen cejanen left a comment

Choose a reason for hiding this comment

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

Nice, comparing to the first attempt I see big progress. Most of changes are logic for me. I tried to find places and noted with questions to confirm my thoughts and eventually we should explain those places in code as well.
There are some minor issues I found and maybe for update like this we should cover library with more multithreading tests.
Some changes should forward us to API design changes itself not just to pass concurrency check without warnings.

Sources/Networking/Core/NetworkingActor.swift Outdated Show resolved Hide resolved
import Foundation

/// The only singleton actor in this library where all networking related operations should synchronize.
@globalActor public actor NetworkingActor {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we have it also final? @globalActor final public actor NetworkingActor? Or are there any downsides?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The final is not really necessary since actors are non-inheritable, but there is no downside to include it if you want.

@@ -16,7 +16,7 @@ final class UploadsViewModel: ObservableObject {

private let uploadService: UploadService

init(uploadService: UploadService = UploadService()) {
init(uploadService: UploadService = .shared) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm a little bit confused. In FormUploadsViewModel we remove init with UploadService injection and we have just let variable with shared instance. I would prefer to have everywhere option to inject the service at least for the showcase, no matter we use just shared instance for now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The service will be removed in #59

init(uploadService: UploadService = .init()) {
self.uploadService = uploadService
}
private let uploadService = UploadService.shared
Copy link
Collaborator

Choose a reason for hiding this comment

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

why did we remove init method? I commented on UploadsViewModel the different approach we used

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The service will be removed in #59

@@ -63,13 +64,13 @@ public protocol UploadAPIManaging {
/// i.e., `UploadTask.State.error` is non-nil. In such case, you can call `retry(taskId:)` to re-activate the stream for the specified `uploadTaskId`.
/// - Parameter uploadTaskId: The identifier of the task to observe.
/// - Returns: An asynchronous stream of upload state. If there is no such upload task the return stream finishes immediately.
func stateStream(for uploadTaskId: UploadTask.ID) async -> StateStream
func stateStream(for uploadTaskId: UploadTask.ID) -> StateStream
Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar question like for other class. Why do we change async to sync method and vice versa? Because of context where does it run?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think it makes sense for methods that don't necessarily need to be async to let them be sync and this one fulfils that criteria. Don't you agree?


// This would ideally also be a lazy var, however it has to be async, because UIDevice.current.name needs to be called on MainActor.
private var _multipeerConnectivityManager: MultipeerConnectivityManager?
private var multipeerConnectivityManager: MultipeerConnectivityManager? {
Copy link
Collaborator

Choose a reason for hiding this comment

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

in case like this, we touch device just for name. I'm thinking maybe the whole design of networking is bad in this case. For this variable maybe we should inject name to the processor and we don't have to figure out where is it from (app will send it - either device name or custom value)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I know, It's a weird implementation, but otherwise it won't be possible to have a shared instance, because the static let shared cannot execute the MainActor isolated code UIDevice.current.name.

@johnkodes
Copy link
Contributor

@ipek did you try to break this as we discussed? 😄

@ipek
Copy link
Collaborator

ipek commented Jan 15, 2024

@ipek did you try to break this as we discussed? 😄

Soon. Lately I feel like I am juggling several contexts and have 0 work done 😅 .

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.

SWIFT_STRICT_CONCURRENCY compiler check to complete
4 participants