Many users have noticed that since the release of FluentKit 1.48.0 almost a month ago, every one of their Model types is afflicted by a new warning:
Stored property '_id' of 'Sendable'-conforming class 'SomeModel' is mutable
The TL;DR version: There's a corner case in Swift when property wrappers are used by classes that makes it impossible to satisfy Sendable's requirements, but Fluent has to require Sendable models in order to satify those requirements itself. Users can and should suppress the warning by adding @unchecked Sendable conformance to each individual type conforming to Model, ModelAlias, Schema or Fields, as in the following example:
import Fluent
// ****** BEFORE: ******
final class SomeModel: Model {
// ****** AFTER: ******
final class SomeModel: Model, @unchecked Sendable {
static let schema = "some_models"
@ID
var id: UUID?
// ...
}
And that's pretty much all there is to it. For those who are interested, the remainder of this post gives some more detail on how things ended up like this.
It's The Property Wrappers’ World, You're Just Living In It
A number of factors are involved in the problem and this being the chosen solution. Here's a mostly complete list:
- The Fluent property wrappers all have setters for their
wrappedValueproperties, which are critical to the ability to set values on a model. As a result, variables using any of the wrappers must themselves be mutable. Even if there was another way to input values for a model to the database, the setters couldn't be removed without breaking source compatibility completely. - Fluent models are reference types (
classes). This is also something that can't be changed short of major and backwards-incompatible changes to the API (and to a lot of Fluent's guts). - Mutable properties in reference types can't be validated as
Sendable-compliant by the compiler. Even if they are safe in reality - for example, if the properties are protected by a lock at runtime - there's no way for the compiler to figure that out. The@unchecked Sendableescape hatch is intended for that case. - Protocols like
Modelcan requireSendablecompliance on all types conforming to them, but they can not confer the@unchecked Sendableescape hatch on conforming types. So as nice as it would be for Fluent to apply it automatically, the language doesn't allow for it. - It's also not viable to just leave out the
Sendablerequirement for models completely - for various reasons which boil down to "the guts of Fluent aren't built very well but we can't really fix it", non-Sendablemodels make it impossible to make the rest of FluentSendable-safe. - Placing
@unchecked Sendableon models is, unfortunately, lying to the compiler - the models are not reallySendable-safe in practice. This is considered acceptable because models have never been thread-safe- and also because when making them generically thread-safe was tried, it turned out to incur such severe performance penalties that Fluent became completely unusable. But there are parts of Fluent which were previously "safe" (or at least, safe enough) which become unsafe with Concurrency in use, so simply leaving Fluent the way it was wasn't an option either. - Last, but not least, one might ask why it's advised to apply
@unchecked Sendableinstead of, say,@preconcurrency import Fluent. There are three reasons for this:@preconcurrencywould disable allSendablechecking related to Fluent APIs, which is drastic overkill at best (and just a bad idea in general).- Nearly none of Fluent's API actually lives in the
Fluentpackage, but rather in theFluentKitpackage, with the latter (the actual implemenation of Fluent) being@_exportedfrom the former (the Fluent provider for Vapor integration).@_exported, among myriad other problems, has ill-defined semantics, including whether or not an attribute like@preconcurrencyapplies transitively to imports. In testing, the results turned out to be unpredictable. In short,@preconcurrency import Fluentdoesn't always work. - Removing
@_exportedfrom Fluent would be yet another source compatibility break.
So bascially, at the end of day, it's all a kludge, but it was the best choice out of a bunch of bad options. Even the Swift core team was helpless to suggest anything better when I brought the problem to them. I promise to fix it for real in Fluent 5!
P.S.: Speaking of Fluent 5, stay tuned for updates on that subject in the near future 😶
