Skip to content

Commit c5017a1

Browse files
committed
Harden vault S3 readiness probe, fix eventual consistency in upload and move tests
1 parent 4be237f commit c5017a1

3 files changed

Lines changed: 37 additions & 56 deletions

File tree

Tests/CryptomatorCloudAccessIntegrationTests/CloudAccessIntegrationTest.swift

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,33 @@ class CloudAccessIntegrationTest: XCTestCase {
272272
}
273273
}
274274

275+
/// Waits for a vault's `d/` directory structure to become visible on S3.
276+
/// Uses the raw S3 provider (not the vault decorator) to avoid partial-state issues
277+
/// where the vault decorator's `createFolder` is not idempotent on retry.
278+
/// Validates both `fetchItemList` and `fetchItemMetadata` on `d/` since the vault
279+
/// decorator's internal `assertParentFolderExists` relies on `fetchItemMetadata`.
280+
static func waitForVaultReadiness(rawProvider: CloudProvider, vaultPath: CloudPath, attempt: Int = 0) -> Promise<Void> {
281+
let dFolderPath = vaultPath.appendingPathComponent("d")
282+
return rawProvider.fetchItemList(forFolderAt: dFolderPath, withPageToken: nil).then { itemList -> Promise<Void> in
283+
guard let firstChild = itemList.items.first else {
284+
throw CloudProviderError.itemNotFound
285+
}
286+
// Also verify that fetchItemMetadata sees both d/ and its first child (e.g. d/AB/).
287+
// This is the same operation assertParentFolderExists uses internally.
288+
return all(
289+
rawProvider.fetchItemMetadata(at: dFolderPath),
290+
rawProvider.fetchItemMetadata(at: firstChild.cloudPath)
291+
).then { _, _ in () }
292+
}.recover { _ -> Promise<Void> in
293+
if attempt >= 30 {
294+
return Promise(IntegrationTestError.consistencyTimeout)
295+
}
296+
return Promise(()).delay(2.0).then {
297+
return waitForVaultReadiness(rawProvider: rawProvider, vaultPath: vaultPath, attempt: attempt + 1)
298+
}
299+
}
300+
}
301+
275302
// MARK: - fetchItemMetadata Tests
276303

277304
func testFetchItemMetadataForFile() {
@@ -678,7 +705,11 @@ class CloudAccessIntegrationTest: XCTestCase {
678705
let testContent = type(of: self).testContentForFilesInTestFolder
679706
try testContent.write(to: localURL, atomically: true, encoding: .utf8)
680707
let cloudPath = type(of: self).integrationTestRootCloudPath.appendingPathComponent("testFolder/overwriteFolder.txt")
681-
provider.createFolder(at: cloudPath).then { _ in
708+
provider.createFolder(at: cloudPath).then {
709+
// Delay to allow eventual consistency propagation (e.g. S3, pCloud) so that
710+
// uploadFile's pre-upload existence check can detect the just-created folder.
711+
return Promise(()).delay(5.0)
712+
}.then {
682713
return self.provider.uploadFile(from: localURL, to: cloudPath, replaceExisting: true)
683714
}.then { _ in
684715
XCTFail("uploadFile fulfilled to already existing folder with replace existing")
@@ -1086,14 +1117,14 @@ extension CloudProvider {
10861117
/**
10871118
Checks if the item exists at the given cloud path.
10881119

1089-
This method is primarily used as a workaround for providers with eventual consistency. It will repeatedly check if `expectToExist` doesn't match with a delay of 1 second up to a maximum of 3 attempts.
1120+
This method is primarily used as a workaround for providers with eventual consistency. It will repeatedly check if `expectToExist` doesn't match with a delay of 2 seconds up to 10 retries (11 total checks).
10901121
*/
10911122
func repeatedlyCheckForItemExistence(at cloudPath: CloudPath, expectToExist: Bool, attempt: Int = 0) -> Promise<Bool> {
10921123
return checkForItemExistence(at: cloudPath).then { itemExists in
1093-
if itemExists == expectToExist || attempt == 3 {
1124+
if itemExists == expectToExist || attempt == 10 {
10941125
return Promise(itemExists)
10951126
} else {
1096-
return Promise(()).delay(1.0).then {
1127+
return Promise(()).delay(2.0).then {
10971128
return repeatedlyCheckForItemExistence(at: cloudPath, expectToExist: expectToExist, attempt: attempt + 1)
10981129
}
10991130
}

Tests/CryptomatorCloudAccessIntegrationTests/CryptoDecorator/VaultFormat6/VaultFormat6S3IntegrationTests.swift

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class VaultFormat6S3IntegrationTests: CloudAccessIntegrationTest {
4242
return
4343
}
4444
// Wait for Scaleway S3's eventual consistency to catch up after vault creation.
45-
_ = waitForVaultReadiness()
45+
_ = waitForVaultReadiness(rawProvider: cloudProvider, vaultPath: vaultPath)
4646
guard waitForPromises(timeout: 60.0) else {
4747
classSetUpError = IntegrationTestError.oneTimeSetUpTimeout
4848
return
@@ -58,31 +58,6 @@ class VaultFormat6S3IntegrationTests: CloudAccessIntegrationTest {
5858
}
5959
}
6060

61-
/// Waits for the vault's `d/` directory structure to become visible on S3.
62-
/// Uses the raw S3 provider (not the vault decorator) to avoid partial-state issues
63-
/// where the vault decorator's `createFolder` is not idempotent on retry.
64-
private static func waitForVaultReadiness(attempt: Int = 0) -> Promise<Void> {
65-
let dFolderPath = vaultPath.appendingPathComponent("d")
66-
return cloudProvider.fetchItemList(forFolderAt: dFolderPath, withPageToken: nil).then { itemList -> Promise<Void> in
67-
guard !itemList.items.isEmpty else {
68-
if attempt >= 30 {
69-
return Promise(IntegrationTestError.consistencyTimeout)
70-
}
71-
return Promise(()).delay(2.0).then {
72-
return waitForVaultReadiness(attempt: attempt + 1)
73-
}
74-
}
75-
return Promise(())
76-
}.recover { error -> Promise<Void> in
77-
if attempt >= 30 {
78-
return Promise(error)
79-
}
80-
return Promise(()).delay(2.0).then {
81-
return waitForVaultReadiness(attempt: attempt + 1)
82-
}
83-
}
84-
}
85-
8661
override func setUpWithError() throws {
8762
try super.setUpWithError()
8863
let setUpPromise = DecoratorFactory.createFromExistingVaultFormat6(delegate: VaultFormat6S3IntegrationTests.cloudProvider, vaultPath: VaultFormat6S3IntegrationTests.vaultPath, password: "IntegrationTest").then { decorator in

Tests/CryptomatorCloudAccessIntegrationTests/CryptoDecorator/VaultFormat7/VaultFormat7S3IntegrationTests.swift

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class VaultFormat7S3IntegrationTests: CloudAccessIntegrationTest {
4242
return
4343
}
4444
// Wait for Scaleway S3's eventual consistency to catch up after vault creation.
45-
_ = waitForVaultReadiness()
45+
_ = waitForVaultReadiness(rawProvider: cloudProvider, vaultPath: vaultPath)
4646
guard waitForPromises(timeout: 60.0) else {
4747
classSetUpError = IntegrationTestError.oneTimeSetUpTimeout
4848
return
@@ -58,31 +58,6 @@ class VaultFormat7S3IntegrationTests: CloudAccessIntegrationTest {
5858
}
5959
}
6060

61-
/// Waits for the vault's `d/` directory structure to become visible on S3.
62-
/// Uses the raw S3 provider (not the vault decorator) to avoid partial-state issues
63-
/// where the vault decorator's `createFolder` is not idempotent on retry.
64-
private static func waitForVaultReadiness(attempt: Int = 0) -> Promise<Void> {
65-
let dFolderPath = vaultPath.appendingPathComponent("d")
66-
return cloudProvider.fetchItemList(forFolderAt: dFolderPath, withPageToken: nil).then { itemList -> Promise<Void> in
67-
guard !itemList.items.isEmpty else {
68-
if attempt >= 30 {
69-
return Promise(IntegrationTestError.consistencyTimeout)
70-
}
71-
return Promise(()).delay(2.0).then {
72-
return waitForVaultReadiness(attempt: attempt + 1)
73-
}
74-
}
75-
return Promise(())
76-
}.recover { error -> Promise<Void> in
77-
if attempt >= 30 {
78-
return Promise(error)
79-
}
80-
return Promise(()).delay(2.0).then {
81-
return waitForVaultReadiness(attempt: attempt + 1)
82-
}
83-
}
84-
}
85-
8661
override func setUpWithError() throws {
8762
try super.setUpWithError()
8863
let setUpPromise = DecoratorFactory.createFromExistingVaultFormat7(delegate: VaultFormat7S3IntegrationTests.cloudProvider, vaultPath: VaultFormat7S3IntegrationTests.vaultPath, password: "IntegrationTest").then { decorator in

0 commit comments

Comments
 (0)