-
Notifications
You must be signed in to change notification settings - Fork 1
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
base: dev
Are you sure you want to change the base?
Conversation
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.
@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?
Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationStorageManaging.swift
Outdated
Show resolved
Hide resolved
Sources/Networking/Modifiers/Interceptors/LoggingInterceptor.swift
Outdated
Show resolved
Hide resolved
...g/Modifiers/Processors/EndpointRequestStorageProcessor/EndpointRequestStorageProcessor.swift
Show resolved
Hide resolved
@@ -8,7 +8,7 @@ | |||
import Networking | |||
import Foundation | |||
|
|||
final class SampleAuthorizationManager: AuthorizationManaging { | |||
actor SampleAuthorizationManager: AuthorizationManaging { |
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.
What's the different of using this final class SampleAuthorizationManager: AuthorizationManaging, Sendable
versus using an actor? 🤔
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 warning Stored property 'apiManager' of 'Sendable'-conforming class 'SampleAuthorizationManager' has non-sendable type 'APIManager'
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, 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? 😄
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.
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?
NetworkingSampleApp/NetworkingSampleApp/Scenes/Upload/UploadService.swift
Show resolved
Hide resolved
NetworkingSampleApp/NetworkingSampleApp/Scenes/Upload/UploadService.swift
Outdated
Show resolved
Hide resolved
Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationManaging.swift
Outdated
Show resolved
Hide resolved
- Fix build error nonisolated MainActor due to UIDevice usage
…tyManager to actors
@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? |
Co-authored-by: Jan Kodeš <jan.kodes@strv.com>
Co-authored-by: Jan Kodeš <jan.kodes@strv.com>
@matejmolnar did you find some information on whether using a single Global Actor for everything could lead to performance drawbacks? 🤔 Eg. by marking everything |
@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. |
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, 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.
import Foundation | ||
|
||
/// The only singleton actor in this library where all networking related operations should synchronize. | ||
@globalActor public actor NetworkingActor { |
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.
Can we have it also final? @globalActor final public actor NetworkingActor? Or are there any downsides?
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 final
is not really necessary since actors are non-inheritable, but there is no downside to include it if you want.
NetworkingSampleApp/NetworkingSampleApp/API/SampleAuthorizationManager.swift
Outdated
Show resolved
Hide resolved
@@ -16,7 +16,7 @@ final class UploadsViewModel: ObservableObject { | |||
|
|||
private let uploadService: UploadService | |||
|
|||
init(uploadService: UploadService = UploadService()) { | |||
init(uploadService: UploadService = .shared) { |
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'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.
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 service will be removed in #59
init(uploadService: UploadService = .init()) { | ||
self.uploadService = uploadService | ||
} | ||
private let uploadService = UploadService.shared |
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.
why did we remove init method? I commented on UploadsViewModel the different approach we used
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 service will be removed in #59
Sources/Networking/Modifiers/Processors/StatusCodeProcessor.swift
Outdated
Show resolved
Hide resolved
@@ -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 |
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.
Similar question like for other class. Why do we change async to sync method and vice versa? Because of context where does it run?
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 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?
...g/Modifiers/Processors/EndpointRequestStorageProcessor/EndpointRequestStorageProcessor.swift
Outdated
Show resolved
Hide resolved
|
||
// 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? { |
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.
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)
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 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
.
@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 😅 . |
This PR aims to solve this issue #50
Changes:
strict swift concurrency
build setting tocomplete
for both the ios-networking package and for the NetworkingSampleApp.Sendable
ThreadSafeDictionary
and replace with regular dictionaries since we are processing all operations on @NetworkingActor.@preconcurrency
keyword beforeimport
of a given module causing the issues (mainly Combine), with a comment explaining why it had to be done.MultipeerConnectivityManager
andEndpointRequestStorageProcessor
, mainly due to the fact that we want to get a device name by callingUIDevice.current.name
which from now on has to be done onMainActor
. As you can see in the code it's quite tricky to inject aMainActor
parameter into another class/actor that is not aMainActor
, but it can be done, even though it's not elegant at all.