Skip to content

Add wait for pending update method #164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Sep 2, 2021
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
21 changes: 20 additions & 1 deletion Sources/MeiliSearch/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,26 @@ public struct MeiliSearch {
self.updates.getAll(UID, completion)
}

/**
Wait for an update to be processed or failed.

Providing an update id, returned by asynchronous MeiliSearch options, call are made
to MeiliSearch to check if the update has been processed or if it has failed.

- parameter UID: The unique identifier of the `Index`.
- parameter updateId: The id of the update.
- parameter: options Optionnal configuration for timeout and interval
- parameter completion: The completion closure used to notify when the server
**/
public func waitForPendingUpdate(
UID: String,
update: Update,
options: WaitOptions? = nil,
_ completion: @escaping (Result<Update.Result, Swift.Error>
) -> Void) {
self.updates.waitForPendingUpdate(UID, update, options, completion)
}

// MARK: Keys

/**
Expand Down Expand Up @@ -867,5 +887,4 @@ public struct MeiliSearch {
_ completion: @escaping (Result<Dump, Swift.Error>) -> Void) {
self.dumps.status(UID, completion)
}

}
5 changes: 5 additions & 0 deletions Sources/MeiliSearch/Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ public extension MeiliSearch {
/// The input or output JSON is invalid.
case invalidJSON

// TimeOut is reached in a waiting function
case timeOut(timeOut: Double)

// URL is invalid
case invalidURL(url: String? = "")

Expand All @@ -88,6 +91,8 @@ public extension MeiliSearch {
return "Response decoding failed"
case .invalidJSON:
return "Invalid json"
case .timeOut(let timeOut):
return "TimeOut of \(timeOut) is reached"
case .invalidURL(let url):
if let strUrl: String = url {
return "Invalid URL: \(strUrl)"
Expand Down
4 changes: 4 additions & 0 deletions Sources/MeiliSearch/Model/Update.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ public struct Update: Codable, Equatable {
/// The UID of the update.
public let updateId: Int

public init(updateId: Int) {
self.updateId = updateId
}

/// Result type for the Update.
public struct Result: Codable, Equatable {

Expand Down
32 changes: 32 additions & 0 deletions Sources/MeiliSearch/Model/WaitOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Foundation

/**
`WaitOptions` struct represent the options used during a waitForPendingUpdate call.
*/
public struct WaitOptions: Codable, Equatable {

// MARK: Properties

/// Maximum time in seconds before timeOut
public let timeOut: Double

/// Interval in seconds between each status call
public let interval: TimeInterval

// MARK: Initializers
public init(
timeOut: Double? = 5,
interval: TimeInterval? = 0.5
) {
self.timeOut = timeOut ?? 5
self.interval = interval ?? 0.5
}

// MARK: Codable Keys

enum CodingKeys: String, CodingKey {
case timeOut
case interval
}

}
47 changes: 41 additions & 6 deletions Sources/MeiliSearch/Updates.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,13 @@ struct Updates {
_ UID: String,
_ update: Update,
_ completion: @escaping (Result<Update.Result, Swift.Error>) -> Void) {

self.request.get(api: "/indexes/\(UID)/updates/\(update.updateId)") { result in

switch result {
case .success(let data):

guard let data: Data = data else {
completion(.failure(MeiliSearch.Error.dataNotFound))
return
}

do {
let result: Update.Result = try Constants.customJSONDecoder.decode(
Update.Result.self,
Expand All @@ -42,9 +38,7 @@ struct Updates {
case .failure(let error):
completion(.failure(error))
}

}

}

func getAll(
Expand Down Expand Up @@ -74,7 +68,48 @@ struct Updates {
}

}
}

private func checkStatus(
_ UID: String,
_ update: Update,
_ options: WaitOptions,
_ startingDate: Date,
_ completion: @escaping (Result<Update.Result, Swift.Error>) -> Void) {
self.get(UID, update) { result in
switch result {
case .success(let status):
if status.status == Update.Status.processed || status.status == Update.Status.failed {
completion(.success(status))
} else if 0 - startingDate.timeIntervalSinceNow > options.timeOut {
completion(.failure(MeiliSearch.Error.timeOut(timeOut: options.timeOut)))
} else {
usleep(useconds_t(options.interval * 1000000))
self.checkStatus(UID, update, options, startingDate, completion)
}
case .failure(let error):
completion(.failure(error))
return
}
}
}

func waitForPendingUpdate(
_ UID: String,
_ update: Update,
_ options: WaitOptions? = nil,
_ completion: @escaping (Result<Update.Result, Swift.Error>) -> Void) {
let currentDate = Date()
let waitOptions: WaitOptions = options ?? WaitOptions()

self.checkStatus(UID, update, waitOptions, currentDate) { result in
switch result {
case .success(let status):
completion(.success(status))
case .failure(let error):
completion(.failure(error))
}
}
}

}
160 changes: 154 additions & 6 deletions Tests/MeiliSearchIntegrationTests/UpdatesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ private struct Movie: Codable, Equatable {

}

private let movies: [Movie] = [
Movie(id: 123, title: "Pride and Prejudice", comment: "A great book"),
Movie(id: 456, title: "Le Petit Prince", comment: "A french book"),
Movie(id: 2, title: "Le Rouge et le Noir", comment: "Another french book"),
Movie(id: 1, title: "Alice In Wonderland", comment: "A weird book"),
Movie(id: 1344, title: "The Hobbit", comment: "An awesome book"),
Movie(id: 4, title: "Harry Potter and the Half-Blood Prince", comment: "The best book"),
Movie(id: 42, title: "The Hitchhiker's Guide to the Galaxy"),
Movie(id: 1844, title: "A Moreninha", comment: "A Book from Joaquim Manuel de Macedo")
]

class UpdatesTests: XCTestCase {

private var client: MeiliSearch!
Expand Down Expand Up @@ -62,12 +73,9 @@ class UpdatesTests: XCTestCase {
let documents: Data = try! JSONEncoder().encode([movie])

self.client.addDocuments(UID: self.uid, documents: documents, primaryKey: nil) { result in

switch result {
case .success(let update):

self.client.getUpdate(UID: self.uid, update) { (result: Result<Update.Result, Swift.Error>) in

switch result {
case .success(let update):
XCTAssertEqual("DocumentsAddition", update.type.name)
Expand All @@ -76,9 +84,7 @@ class UpdatesTests: XCTestCase {
XCTFail()
}
expectation.fulfill()

}

case .failure:
XCTFail("Failed to update movie index")
}
Expand All @@ -101,7 +107,6 @@ class UpdatesTests: XCTestCase {
}

self.client.getAllUpdates(UID: self.uid) { (result: Result<[Update.Result], Swift.Error>) in

switch result {
case .success(let updates):
updates.forEach { (update: Update.Result) in
Expand All @@ -115,11 +120,154 @@ class UpdatesTests: XCTestCase {
expectation.fulfill()

}
self.wait(for: [expectation], timeout: 10.0)

}

func testWaitForPendingUpdateSuccessDefault () {
let expectation = XCTestExpectation(description: "Wait for pending update with default options")

self.client.addDocuments(
UID: self.uid,
documents: movies,
primaryKey: nil
) { result in

switch result {
case .success(let update):
XCTAssertEqual(Update(updateId: 0), update)
self.client.waitForPendingUpdate(UID: self.uid, update: update) { result in
switch result {
case .success(let update):
XCTAssertEqual(update.status, Update.Status.processed)
case .failure(let error):
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
case .failure(let error):
XCTFail(error.localizedDescription)
}
}
self.wait(for: [expectation], timeout: 10.0)
}

func testWaitForPendingUpdateSuccessEmptyOptions () {
let expectation = XCTestExpectation(description: "Wait for pending update with default options")

self.client.addDocuments(
UID: self.uid,
documents: movies,
primaryKey: nil
) { result in

switch result {
case .success(let update):
XCTAssertEqual(Update(updateId: 0), update)
self.client.waitForPendingUpdate(UID: self.uid, update: update, options: WaitOptions()) { result in
switch result {
case .success(let update):
XCTAssertEqual(update.status, Update.Status.processed)
case .failure(let error):
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
case .failure(let error):
print(error)
XCTFail()
}
}
self.wait(for: [expectation], timeout: 5.0)
}

func testWaitForPendingUpdateSuccessWithOptions () {
let expectation = XCTestExpectation(description: "Wait for pending update with default options")

self.client.addDocuments(
UID: self.uid,
documents: movies,
primaryKey: nil
) { result in

switch result {
case .success(let update):
XCTAssertEqual(Update(updateId: 0), update)
self.client.waitForPendingUpdate(UID: self.uid, update: update, options: WaitOptions(timeOut: 5, interval: 2)) { result in
switch result {
case .success(let update):
XCTAssertEqual(update.status, Update.Status.processed)
case .failure(let error):
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
case .failure(let error):
XCTFail(error.localizedDescription)
}
}
self.wait(for: [expectation], timeout: 5.0)
}

func testWaitForPendingUpdateSuccessWithIntervalZero () {
let expectation = XCTestExpectation(description: "Wait for pending update with default options")
self.client.addDocuments(
UID: self.uid,
documents: movies,
primaryKey: nil
) { result in
switch result {
case .success(let update):
XCTAssertEqual(Update(updateId: 0), update)
self.client.waitForPendingUpdate(UID: self.uid, update: update, options: WaitOptions(timeOut: 5, interval: 0)) { result in
switch result {
case .success(let update):
XCTAssertEqual(update.status, Update.Status.processed)
case .failure(let error):
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
case .failure(let error):
XCTFail(error.localizedDescription)
}
}
self.wait(for: [expectation], timeout: 5.0)
}

func testWaitForPendingUpdateTimeOut () {
let expectation = XCTestExpectation(description: "Wait for pending update with default options")

self.client.addDocuments(
UID: self.uid,
documents: movies,
primaryKey: nil
) { result in
switch result {
case .success(let update):
XCTAssertEqual(Update(updateId: 0), update)
self.client.waitForPendingUpdate(UID: self.uid, update: update, options: WaitOptions(timeOut: 0, interval: 2)) { result in
switch result {
case .success:
XCTFail("waitForPendingUpdate should not have had the time for a second call")
case .failure(let error):
print(error.localizedDescription)
switch error {
case MeiliSearch.Error.timeOut(let double):
XCTAssertEqual(double, 0.0)
default:
XCTFail("MeiliSearch TimeOut error should have been thrown")
}
}
expectation.fulfill()
}
case .failure(let error):
print(error)
XCTFail()
}
}
self.wait(for: [expectation], timeout: 5.0)
}
}
// swiftlint:enable force_unwrapping
// swiftlint:enable force_try