Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Split.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = 'Split'
s.module_name = 'Split'
s.version = '3.5.2-rc1'
s.version = '3.5.2-rc2'
s.summary = 'iOS SDK for Split'
s.description = <<-DESC
This SDK is designed to work with Split, the platform for controlled rollouts, serving features to your users via the Split feature flag to manage your complete customer experience.
Expand Down
2 changes: 1 addition & 1 deletion Split/Common/Utils/Version.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
class Version {
private static let kSdkPlatform: String = "ios"

private static let kVersion = "3.5.2-rc1"
private static let kVersion = "3.5.2-rc2"

static var semantic: String {
return kVersion
Expand Down
8 changes: 8 additions & 0 deletions Split/Storage/CoreDataHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ class CoreDataHelper {
}
}

/// Roll back any unsaved changes in the managed object context.
/// Useful after a failed save(), to prevent the context from keeping invalid pending changes.
func rollback() {
managedObjectContext.performAndWait {
self.managedObjectContext.rollback()
}
}

private func delete(entity: CoreDataEntity, predicate: NSPredicate? = nil) {

managedObjectContext.performAndWait {
Expand Down
7 changes: 5 additions & 2 deletions Split/Storage/Splits/PersistentSplitsStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ class DefaultPersistentSplitsStorage: PersistentSplitsStorage {
}

func update(splitChange: ProcessedSplitChange, onFailure: ((Error) -> Void)? = nil) {
// Execute transactionally: all operations must succeed or all must fail
coreDataHelper.performAndWait { [weak self] in
// This is intentionally async to avoid blocking the caller thread.
// All operations must succeed or all must fail.
coreDataHelper.perform { [weak self] in
guard let self = self else { return }

do {
Expand All @@ -55,6 +56,8 @@ class DefaultPersistentSplitsStorage: PersistentSplitsStorage {
try self.coreDataHelper.saveWithErrorHandling()
} catch {
Logger.e("Transactional flags update failed: \(error.localizedDescription)")
// Rollback to avoid leaving invalid pending changes in the shared context,
self.coreDataHelper.rollback()
onFailure?(error)
}
}
Expand Down
5 changes: 5 additions & 0 deletions SplitTests/Fake/Storage/CoreDataHelperStub.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class CoreDataHelperStub: CoreDataHelper {

var shouldFailOnSave = false
var saveError: Error = NSError(domain: "TestCoreData", code: 500, userInfo: [NSLocalizedDescriptionKey: "Simulated save failure"])
var rollbackCalled = false

init() {
let model = NSManagedObjectModel()
Expand Down Expand Up @@ -39,5 +40,9 @@ class CoreDataHelperStub: CoreDataHelper {
}
// Success
}

override func rollback() {
rollbackCalled = true
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -243,14 +243,19 @@ class EncryptionKeyValidationIntegrationTest: XCTestCase {
}
wait(for: [checkExp], timeout: 3)

// 2. Disable encryption
// 2. Create a fresh dbHelper
dbHelper = IntegrationCoreDataHelper.get(databaseName: testDbName,
dispatchQueue: DispatchQueue.global())
let freshGeneralInfoDao = CoreDataGeneralInfoDao(coreDataHelper: dbHelper)

// 3. Disable encryption
let factory = createFactory(encryptionEnabled: false)
waitForReady(factory: factory)

// 3. Verify verifier is removed
// 4. Verify verifier is removed
let verifyExp = expectation(description: "Verifier removed")
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
XCTAssertNil(generalInfoDao.stringValue(info: .encryptionVerifier))
XCTAssertNil(freshGeneralInfoDao.stringValue(info: .encryptionVerifier))
verifyExp.fulfill()
}
wait(for: [verifyExp], timeout: 3)
Expand Down
21 changes: 21 additions & 0 deletions SplitTests/Storage/CoreDataHelperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,26 @@ class CoreDataHelperTests: XCTestCase {
// Context without persistent store should not throw when there are no changes
XCTAssertNoThrow(try invalidHelper.saveWithErrorHandling())
}

func testRollbackClearsInvalidPendingChangesAndAllowsNextSave() {
// Create an invalid entity that will cause a validation failure on save.
coreDataHelper.performAndWait {
_ = self.coreDataHelper.create(entity: .generalInfo)
}

XCTAssertThrowsError(try coreDataHelper.saveWithErrorHandling())

// Rollback should clear the invalid pending changes so future saves can succeed.
coreDataHelper.rollback()

coreDataHelper.performAndWait {
if let entity = self.coreDataHelper.create(entity: .generalInfo) as? GeneralInfoEntity {
entity.name = "post_rollback_ok"
entity.stringValue = "value"
}
}

XCTAssertNoThrow(try coreDataHelper.saveWithErrorHandling())
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ class PersistentSplitsStorageTransactionalTests: XCTestCase {
XCTAssertNotNil(reportedError, "Error should be reported")
}

func testRollbackIsInvokedOnSaveError() {
coreDataHelperStub.shouldFailOnSave = true

let change = ProcessedSplitChange(
activeSplits: [createSplit(name: "split1")],
archivedSplits: [],
changeNumber: 100,
updateTimestamp: 1000
)

splitsStorage.update(splitChange: change, onFailure: { _ in })

XCTAssertTrue(coreDataHelperStub.rollbackCalled, "Rollback should be invoked when transactional save fails")
}

func testNilFailureCallbackIsHandled() {
let change = ProcessedSplitChange(
activeSplits: [createSplit(name: "split1")],
Expand Down
Loading