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

Add Sendable Conformances to Vapor #3000

Closed
wants to merge 87 commits into from
Closed

Add Sendable Conformances to Vapor #3000

wants to merge 87 commits into from

Conversation

0xTim
Copy link
Member

@0xTim 0xTim commented Apr 17, 2023

Adds Sendable conformances where appropriate. This removes almost all of the warnings when building Vapor with strict concurrency checking. Fixes a large number of concurrency and thread-safety issues caused by the introduction of Swift concurrency.

Huge thanks to @dnadoba @Lukasa and everyone else who helped getting this over the line!

This also drops support for Swift 5.7 because of the use of @preconcurrency

Copy link

@dnadoba dnadoba left a comment

Choose a reason for hiding this comment

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

Looked at a small portion of it and left some nits on the way. Looks good overall though!

Sources/Vapor/Bcrypt/Bcrypt.swift Outdated Show resolved Hide resolved
Comment on lines 62 to 64
private final class Cache: @unchecked Sendable {
private var storage: [ObjectIdentifier: Any]

private let storageLock: NIOLock
Copy link

Choose a reason for hiding this comment

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

Use NIOLockedValueBox instead of storageLock and separate storage. The compiler can then infere the Sendable conformance and you can drop @unchecked. This will also allow you to make this a struct instead of a class as NIOLockedValueBox is already a reference type. Document that the struct has reference semantics now.

Copy link
Member Author

Choose a reason for hiding this comment

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

I can't use NIOLockedValueBox though because of the use of Any right? I can't guarantee that anything being inserted into the storage is Sendable at which point NIOLockedValueBox is not Sendable

Copy link
Contributor

Choose a reason for hiding this comment

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

True, but given that Cache is anyways @unchecked Sendable due to storage not being Sendable, it wouldn't matter if the NIOLockedValueBox isn't Sendable either, right?

Copy link
Member

Choose a reason for hiding this comment

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

I see what you mean @ffried. It's not a huge deal either way.

Sources/Vapor/Bcrypt/Bcrypt.swift Outdated Show resolved Hide resolved
@0xTim 0xTim linked an issue Apr 24, 2023 that may be closed by this pull request
@0xTim 0xTim marked this pull request as ready for review May 30, 2023 15:05
@0xTim 0xTim linked an issue May 30, 2023 that may be closed by this pull request
@0xTim 0xTim added the semver-minor Contains new API label May 30, 2023
@codecov-commenter
Copy link

codecov-commenter commented May 30, 2023

Codecov Report

Merging #3000 (ea81637) into main (e98077d) will decrease coverage by 0.03%.
The diff coverage is 83.79%.

❗ Current head ea81637 differs from pull request most recent head 3c2e23a. Consider uploading reports for the commit 3c2e23a to get more accurate results

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3000      +/-   ##
==========================================
- Coverage   76.72%   76.69%   -0.03%     
==========================================
  Files         211      211              
  Lines        7781     8021     +240     
==========================================
+ Hits         5970     6152     +182     
- Misses       1811     1869      +58     
Impacted Files Coverage Δ
Sources/Vapor/Authentication/Authenticator.swift 57.89% <ø> (ø)
...rces/Vapor/Authentication/BasicAuthorization.swift 59.25% <ø> (ø)
...ces/Vapor/Authentication/BearerAuthorization.swift 58.82% <ø> (ø)
Sources/Vapor/Cache/CacheExpirationTime.swift 70.00% <ø> (ø)
Sources/Vapor/Client/Client.swift 46.66% <ø> (ø)
Sources/Vapor/Client/ClientRequest.swift 38.09% <ø> (ø)
Sources/Vapor/Client/ClientResponse.swift 41.66% <ø> (ø)
...es/Vapor/Concurrency/AnyResponse+Concurrency.swift 0.00% <ø> (ø)
...ources/Vapor/Concurrency/AsyncBasicResponder.swift 100.00% <ø> (ø)
.../Concurrency/AsyncPasswordHasher+Concurrency.swift 50.00% <ø> (ø)
... and 128 more

@0xTim 0xTim mentioned this pull request Jun 13, 2023
@stevapple
Copy link
Sponsor Contributor

This also drops support for Swift 5.7 because of the use of @preconcurrency

Didn't expect to drop 5.7 before 5.6 LOL. A tiny (yet key) typo.

Copy link
Contributor

@ffried ffried left a comment

Choose a reason for hiding this comment

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

Came across this, waiting for it to drop :)

Left quite a few review comments (sorry for the noise!) - most of them are just small inconsistencies.
Maybe some of them aren't valid, e.g. if you always explicitly declare conformances (instead of relying on inheritance).

Sources/Vapor/Application.swift Show resolved Hide resolved

public final class Locks {
// This does not have value semantics due to the use or `NIOLockedValueBox`
public struct Locks: Sendable {
Copy link
Contributor

Choose a reason for hiding this comment

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

Changing this to a struct is theoretically semver-breaking:

func useClass<C: AnyObject>(_ c: C) {}

useClass(Application().locks) // now fails

Could be acceptable, though, since I don't see a reasonable use-case for such a function.

Copy link
Member

Choose a reason for hiding this comment

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

This is very much a breaking change. You could do access let locks = application.locks and any access to locks would mismatch the state in application.locks

Copy link
Contributor

Choose a reason for hiding this comment

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

@Joannis I don't think so. All properties of Locks have reference semantics (NIOLock is a class and NIOLockedValueBox has documented reference semantics). So Locks itself essentially still has reference semantics as well.

Sources/Vapor/Application.swift Show resolved Hide resolved
Comment on lines 62 to 64
private final class Cache: @unchecked Sendable {
private var storage: [ObjectIdentifier: Any]

private let storageLock: NIOLock
Copy link
Contributor

Choose a reason for hiding this comment

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

True, but given that Cache is anyways @unchecked Sendable due to storage not being Sendable, it wouldn't matter if the NIOLockedValueBox isn't Sendable either, right?

@@ -12,19 +12,20 @@ extension Application {

public struct Clients {
public struct Provider {
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't Provider become Sendable?

@@ -22,7 +22,7 @@ extension Array where Element == CodingKey {
}

/// A basic `CodingKey` implementation.
public enum BasicCodingKey: CodingKey, Hashable {
public enum BasicCodingKey: CodingKey, Hashable, Sendable {
Copy link
Contributor

Choose a reason for hiding this comment

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

CodingKey already requires Sendable.

@@ -24,7 +24,7 @@ public struct File: Codable, Equatable {
}
}

enum CodingKeys: String, CodingKey {
enum CodingKeys: String, CodingKey, Sendable {
Copy link
Contributor

Choose a reason for hiding this comment

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

CodingKey already requires Sendable.

self.run = run
}
}

// This doesn't need a lock as it's only mutated during app configuration
final class Storage {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can become Sendable as well (like other Storage's).

@@ -22,7 +22,7 @@ final class ApplicationTests: XCTestCase {
}

func testLifecycleHandler() throws {
final class Foo: LifecycleHandler {
final class Foo: LifecycleHandler, @unchecked Sendable {
Copy link
Contributor

Choose a reason for hiding this comment

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

@unchecked is not needed, is it?

/// It can be used to capture local mutable values in a `@Sendable` closure and mutate them from within the closure.
/// As the name implies, the usage of this is unsafe because it disables the sendable checking of the compiler and does not add any synchronisation.
@usableFromInline
final class UnsafeMutableTransferBox<Wrapped> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is only used in this file as far as I've seen. Maybe make it fileprivate for now to prevent mis-use?

@chibombo
Copy link

chibombo commented Nov 1, 2023

Will this PR fix the Vapor @preconcurrency warnings?

@preconcurrency /mydir/.build/checkouts/randomfile.swift:1:1: remark: add '@preconcurrency' to suppress 'Sendable'-related warnings from module 'Vapor' import Vapor

@0xTim
Copy link
Member Author

0xTim commented Nov 1, 2023

@chibombo the idea is yes but this PR won't be merged. We've been adding Sendable stuff in piece-by-piece as we learn more about it etc, including the recent #3082 that just got merged.

I think we're just down to Request now which needs to be migrated, which is the hardest one 😅

Once that's in, the warning should disappear

@0xTim
Copy link
Member Author

0xTim commented Nov 1, 2023

Closing this as we've been doing this in different parts

@0xTim 0xTim closed this Nov 1, 2023
@0xTim 0xTim deleted the sendable-request branch November 1, 2023 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver-minor Contains new API
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Vapor.Request isn't thread safe but a mutable class Make WebSocket conform to Sendable
7 participants