I have a couple of models which I'm looking to send via API. My models look like this:
@Model
final class ModelElement:Hashable, Identifiable, Codable {
@Attribute(.unique) var id: String?
var name:Int?
var date:Int?
var type:CoreType?
var valueType:ValueType?
var source:Source?
// ...
Elsewhere, I have an API Queue I have set up which sends these models to a backend. The API queue simply queues up APIs so they run serially, but apart from that it's a typical API call.
I have it setup this way since I want to save these models to my modelContext after they have been successfully sent to the backend:
However I am now getting the error "Sending 'newElement' risks causing data races; this is an error in the Swift 6 language mode. I understand the error, since a model is not sendable, but I am trying to figure out the best way to solve it. I could create a separate version of the data where each property uses "let" rather than "var", but I don't really want to duplicate the definition if I can avoid it. I also don't think it makes sense to call the model itself sendable, since it is not.
Is there an easy way to isolate the ModelElement so I can send it without the warning? Realistically, it will not be changed, but I'd like to avoid over use of nonisolated(unsafe) or similar. What's the ideal way to write this code in Swift 6?
Hi @smpnjn, ideally you wouldn't need to make ModelElement sendable, and instead your APIs could be constructed in such a way that makes it safe to use the model even though it is not sendable. However, I think you'll need to share a bit more of your code because with what you have shared so far there technically shouldn't be a problem. It is totally fine to pass a non-sendable object to an async method as this code shows:
class Model /*Not Sendable*/ {
var count = 0
}
func write<T>(_ object: T) async {}
func submit() async {
let model = Model()
await write(model) // 👍
}
Where things can go wrong is if you start accessing that non-sendable object in a sending closure as well as in the submit function because you start crossing isolation regions.
So, what else is happening inside submitItem and where exactly is the error?
Thanks for your response. I am using an actor for the API queue (may be part of the problem), but I would like API calls to be ran by a separate thread if possible. Here is my code for the APIQueue (which is the api.call line). This setup allows me to queue the API calls serially, so that each waits until the last one responds before firing:
actor APIQueue {
enum TaskStatus {
case inProgress(Task<Data?, Error>)
case complete(Data)
}
private var queue: [UUID: TaskStatus] = [:]
let baseUrl = "https://example.dev/api/v1"
func call<T: Decodable & Encodable & Sendable>(key: UUID, data: T, method: String, url: String) async -> Data? {
if case let .complete(data) = queue[key] {
return data
}
if case let .inProgress(task) = queue[key] {
return try? await task.value
}
do {
let task: Task<Data?, Error> = Task {
let data = try await APIEvent.authCall(
key: key,
data: data,
method: method,
url: "\(baseUrl)\(url)"
)
return data
}
queue[key] = .inProgress(task)
if let data = try await task.value {
queue[key] = .complete(data)
return data
} else {
queue[key] = nil
return nil
}
} catch {
return nil
}
}
}
I then have a static function elsewhere in my code which is where I get "authCall" from:
I see. I think you are going to want to drop the Sendable constraint in call and authCall. That will allow you to pass along non-sendable things.
Of course that won't compile because you are capturing data inside Task { … }, however I think it's quite easy to workaround that. Currently you capture data in Task { … } so that you an pass it to authCall, and then all authCall does is encode the object so that it can be sent with the request. So, instead, pass Data to authCall and move the encoding work to call, and in particular do it before spinning up the Task. That will allow call to take a non-sendable object while not falling afoul of the isolation regions.
I'm pretty certain that will work, but let me know if there is an issue that I am not seeing from your code.