Skip to content
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

atomic operation issue #98

Closed
foolbear opened this issue Nov 12, 2017 · 8 comments
Closed

atomic operation issue #98

foolbear opened this issue Nov 12, 2017 · 8 comments

Comments

@foolbear
Copy link

foolbear commented Nov 12, 2017

Existence must be checked before insert a record into table. but these are 2 operations(check&insert), when multiple users do this in the same time, it will cause issue of inserting 2 same records.

How to handle this?

UserA: |----> check ----> not existence ----> insert ----> insert succ
UserB: |--------> check ----> not existence ----> insert ----> insert succ

So 2 same records will be inserted into table.

@foolbear foolbear reopened this Nov 14, 2017
@foolbear
Copy link
Author

@evermeer

@evermeer
Copy link
Owner

In this case it would be better to specify your own recordID when creating a new record. If you make sure that it represents your primary key (probably the field that you use in your check) then you could insert your data without validating it first. If the recordID already exists, then you will end up in the failure handler and you can react on that (fetch and update or report an error).

@foolbear
Copy link
Author

In my case, users submit their reading books, there are 2 tables: book(title, author) and reading(who, when book_reference). Steps are: check book existence -> yes, insert reading using existed bookid; no, insert a new book and insert reading using this new bookid.

One day, 2 users submit the same book at almost same moment, 2 the same books will submitted.

This is the issue for all case of insertion according to existence.

How to ensure the record uniqueness using cloudkit?

@evermeer
Copy link
Owner

evermeer commented Nov 14, 2017

CloudKit keeps a version number with every record. So if both user A and B read from CloudKit and they will both get version X, Then whoever saves first kan just save it because the record will have version X in it's system variables. The version in CloudKit will be changed to Y by this save operation. If user B tries to save the same record, then CloudKit will give an error because version X from the save is not the same as Y in the latest version. You can then react on that situation depending on what you want (re read and update or cancel)

It's not that hard to create a unit test for that. It will look something like:
var myObj1 = .... read object
var myObj2 = ... read same object.
change myObj1 and save
change myObj2 and save
That last save will end up in the error callback.

@foolbear
Copy link
Author

but when the record is not exist before two users checking, not any version for no exist recored.

In normal DB system, the index mechanism ensure that the same record could not be insert 2 times by index field.

In some server system, a lock mechanism is provided. lock object, do operation with object, then unlock it. This mechanism ensure only a instance/user to do with the object.

Does Cloudkit provide the similar mechanisms? Batch operation? Or Insert a record whatever the record existence, it will automatic insert and return new record or just return a existence record that just like your saving function (save or update).

@evermeer
Copy link
Owner

That's why you need a unique specific RecordId for the record. If person 1 saves that record, the 2nd can not save the same ID. He will get an error indicating that it already exists and needs to do a read and update. So you could say that the RecordId works like the primary index. There is no option to lock a record. You need to use the version mechanism for that.

CloudKit does have support for bach operations (for instance use SaveItems instead of SaveItem). The mechanism described above will always work. You need to handle a failed save and react the way you would like to. EVCloudKitDao stays as close to the CloudKit implementation as possible. It's only a collection of convenience functions. As a layer on top of that EVCloudData has the .connect which joins the query and subscription functionality in one.

@foolbear
Copy link
Author

foolbear commented Nov 16, 2017

Q1:How to assign RecordId in saveItem function? We can assign RecordId ourself for Cloudkit?

Q2:Does code look like following?

	func getOrNewBookC(_ bookFS: BookFS, completion_: @escaping (CKBook?) -> Void) {
		EVCloudKitDao.publicDB.query(CKBook(), predicate: NSPredicate(format: "title == %@ && author == %@", bookFS.title, bookFS.author), completionHandler: { results, _ in
			if results.count > 0 {
				let cKBook = results[0]
				completion_(cKBook)
			} else {
				let cKBook = CKBook()
**Q2:cKBook.RecordId = self.generateUUID()**
				cKBook.title = bookFS.title
				cKBook.author = bookFS.author
				EVCloudData.publicDB.saveItem(cKBook, completionHandler: { cKBook in
					completion_(cKBook)
				}, errorHandler: { error in
					completion_(nil)
**Q3:how to judge this error is "record already exist" ?**
					foolPrint("save CKBook error: \(error.localizedDescription)")
				})
			}
			return false
		}, errorHandler: { error in
			completion_(nil)
			foolPrint("query CKBook error: \(error.localizedDescription)")
		})
	}

Q4: If it's worked, existence checking is not necessary, we just save it with the unique recordid, new if success, exist if failed with specific error. right?

Q5: the same situation for new/update a record?

@evermeer
Copy link
Owner

You could test it with code like this:

    func conflictTest() {
        let message = Message()
        message.recordID = CKRecordID(recordName: "We use this twice")
        message.Text = "This is the message text"

        let message2 = Message()
        message2.recordID = CKRecordID(recordName: "We use this twice")
        message2.Text = "This is an other message text"

        self.dao.saveItem(message, completionHandler: {record in
            EVLog("saveItem Message: \(record.recordID.recordName)");
        }, errorHandler: {error in
            EVLog("<--- ERROR saveItem message \(error)");
        })

        self.dao.saveItem(message, completionHandler: {record in
            EVLog("saveItem Message: \(record.recordID.recordName)");
        }, errorHandler: {error in
            EVLog("<--- ERROR saveItem message \(error)");
        })
    }

You will see this in the output:

11/16/2017 20:52:31:432 AppMessage)[3479:.] TestsViewController.swift(61) conflictTest():
	saveItem Message: We use this twice

11/16/2017 20:52:31:659 AppMessage)[3479:.] TestsViewController.swift(69) conflictTest():
	<--- ERROR saveItem message <CKError 0x1c42477a0: "Server Record Changed" (14/2004); server message = "record to insert already exists"; uuid = 068BF54F-3915-4078-A94B-57D7C9D11CBC; container ID = "iCloud.nl.clockwork.AppMessage">

So in the errorHandler of that 2nd save you could do a .getItem, change data and .save

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants