I have a codable struct in my basic Linux executable which represents the per-install configurable properties. It's called AppConfig and it can read and write itself out to the filesystem.
I was hoping to automate this in a way, where if certain properties in this struct are altered then it would automatically save to disc.
So, I tried a very simple property wrapper and then a didSet property observer which would call the instance method to write the struct out to file, but both of these fail due to **Simultaneous accesses to 0x1011385e0, but modification requires exclusive access.**
From another thread here I see that this is due to ownership/safety - it's not ideal to read a property whilst it's being modified.
I would have thought that this would be safe, as didSet happens after the modification has happened, but I'm sure that there's a sensible implementation reason why this fails.
I suppose my real question is this: How does one get around this? How does one get safely notified that a property has changed?
My first thought was that I could use a short timer to delay the call to the function that writes the struct out to disc and therefore make sure that the 'modification' had passed. But this seems a bit of a hack.
Anyway, I used such approach where I wrote the changes value to the disk in the didSet, and it worked totally fine. I didn't use a property wrapper though, that might be the difference.
Maybe this is the issue: in my actual app there is a web server which acts as a UI (Hummingbird) which operates on not the main thread. The Config struct is a global variable. Maybe the web server is 'holding on' to the struct from another thread?
The standard exclusivity checks aren't thread safe, so if the exclusivity violation is happening across threads, you may not reliably see the check. However, if you enable Thread Sanitizer, you should get cross-thread exclusivity checking as well, along with the general data race checking TSan does. That might help reproduce the issue.
Thanks Joe. I'm not that familiar with multi-threading so will have to read-up on TSan (and threading in general). It looks like it's not the web server, maybe a race as both accesses are on the same thread and both in my own code.
That's the issue then: that global is getting accessed from more than one thread and that's a problem. Easy to test:
private var _realValue: T = ...
var value: T {
get {
dispatchPrecondition(.onQueue(theQueue))
return _realValue
}
set {
dispatchPrecondition(.onQueue(theQueue))
_realValue = newValue
}
}
Standard solutions: serialise access by using a lock, a queue, an actor.
OK, I've managed to reduce it down in the playground. Please excuse my ignorance - I'm sure I'm doing something very wrong here.
So - my initial Playground test used property observers and didn't crash, but when I added a property wrapper, it crashed just like the main project in Xcode.
// AppConfiguration.swift
import Foundation
@propertyWrapper struct AutoSaveStringToDisc: Codable {
var wrappedValue: String {
// Delayed
// didSet {
// // Delay call to write out slightly so that we aren't in the mutation phase
// let interval = 0.25
// let delayTimer = Timer(fire: Date() + interval, interval: interval, repeats: false, block: {(t:Timer) -> Void in
// _ = config.persistConfigToFilesystem()
// })
// RunLoop.main.add(delayTimer, forMode: RunLoop.Mode.common)
// }
didSet {
_ = config.persistConfigToFilesystem()
}
}
}
// MARK: - AppConfig -
/// Struct to hold the global Config for the app. Stored as JSON on file between launches.
public struct AppConfiguration: Codable {
// Ignore the enums that aren't codable
enum CodingKeys: CodingKey {
case configFolderPath
case stringOne
case stringTwo
}
// MARK: - Properties -
var configFolderPath: String = "/tmp"
var stringOne: String = "One" {
didSet {
persistConfigToFilesystem()
}
}
var stringTwo: String = "Two" {
didSet {
persistConfigToFilesystem()
}
}
@AutoSaveStringToDisc var stringThree: String = "Three"
// MARK: - Functions -
/// Prints the config as JSON - Useful for copying to a file.
func spitOutJSON() {
print("INFO: Spitting out current config as JSON")
do {
let encodedData = try JSONEncoder().encode(self)
let jsonString = String(data: encodedData, encoding: .utf8)
print(jsonString ?? "Could not encode JSON")
} catch {
print(error)
}
}
/// Writes the config out to flie as JSON
func persistConfigToFilesystem() -> Bool {
print("persisting appConfig to disc.")
let path = configFolderPath + "/testConfig.json"
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
do {
let url = URL(fileURLWithPath: path)
let data = try encoder.encode(self)
// The folder should be guaranteed to exist due to the housekeeping() in GateControl.
try data.write(to: url, options: [.atomic])
} catch {
print("Could not write appConfig JSON to: \(path) - \( error)")
return false
}
return true
}
mutating func loadConfigFromFilesystem() {
let importPath = configFolderPath + "/testConfig.json"
print("INFO: loading config from filesystem \(importPath)")
var importedConfig: AppConfiguration
do {
if let importedJsonData = try String(contentsOfFile: importPath).data(using: .utf8) {
importedConfig = try JSONDecoder().decode(AppConfiguration.self, from: importedJsonData)
} else {
print("ERROR: AppConfig: could not convert string to JSON \(importPath)")
return
}
} catch {
print("ERROR: AppConfig: Could not load config from JSON - \(importPath) - \(error)")
return
}
// We should now have a valid config instance.
// Swap ourselves for the imported instance.
self = importedConfig
}
}
class ClientClass {
// The current Timing Profile.
var activeProfile = String()
{
didSet {
// primeEventWatcher()
config.stringOne = activeProfile
config.stringThree = activeProfile
}
}
}
// MARK: - Entry Point -
var config = AppConfiguration()
config.stringOne = "Hello"
config.stringTwo = "World"
config.stringThree = "DubDub Next Week..."
//testConfig.persistConfigToFilesystem()
let client = ClientClass()
client.activeProfile = "Test"
I wouldn't trust playground: it's constantly reading variables (to show their current state) which might conflict with your writes to those variables. When I run your example in a playground I can see the crash you are talking about but when I run it as a console command line app it runs fine.
Yeah, this code is incorrect. Specifically the recursive relationship between TestPropertyWrapper and global via var stringThree is not safe.
When you do global.stringThree = activeProfile in ClientClass, you begin a mutable access of global. Within that mutable access of global, the TestPropertyWrapper that wraps var stringThree makes a read access of global indirectly. This is forbidden: for the duration of the mutating access of global you are not allowed to touch the variable global from anywhere. This is The Law of Exclusivity.
I wonder why I am not getting exclusivity violation runtime errors with this example. Double checked - the relevant settings are enabled in the project.
There has been spotty enforcement of this in some cases in the past: Exclusive access duration relative to didSet. I'd recommend filing a bug on GitHub with a complete description of your environment (Xcode version, Swift version, macOS version, hardware version) and your project or compiler command line and test environment. That will let the Swift team nail it down.