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

replaceOrCreate should create new instance if id provided #54

Closed
wants to merge 2 commits into from

Conversation

jannyHou
Copy link
Contributor

@jannyHou jannyHou commented Nov 8, 2016

Connect to https://github.com/strongloop-internal/scrum-loopback#991

Scenario:
Without fix in this pr

MyModel.replaceOrCreate({
  id: 'myid', // id provided 
  name: 'myname',
}, function(err, result){...});

will fail if the instance doesn't exist in database.
The current code doesn't check instance exist before do the getRevision() , which cannot run against an nonexistent instance.

Details see: https://github.com/strongloop-internal/scrum-loopback/issues/991#issuecomment-256077756

@jannyHou jannyHou added the bug label Nov 8, 2016
@jannyHou jannyHou self-assigned this Nov 8, 2016
@jannyHou
Copy link
Contributor Author

jannyHou commented Nov 8, 2016

The change comparison looks messy..
Let me explain a little bit what I changed in this pr:
I just moved the logic if(id && dataExist){...}else{...} into callback of function self.count(), so now the logic turns to:

self.count({condition_to_check_instance_exist}, function(err, cnt) {
  dataExist = cnt === 1 ? true : false;
  if (id && dataExist) {
    // Do Replace, make sure getRevision won't run on an nonexistent instance
  } else {
    // Do Create
  }
})

@Amir-61 @bajtos PTAL. Thanks :)

Copy link

@Amir-61 Amir-61 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jannyHou Reviewed. Also please squash all your commits as one single commit.

verifyCreatedData(product);
});

function verifyCreatedData(data) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would chose a better function name; how about verifyReturnedCallbackData?

data.price.should.be.equal(100);

// Verify DB data
verifyDBData(data.id);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the method name is/should be descriptive enough so the comment is unnecessary in this case...

verifyDBData(data.id);
};

function verifyDBData(id) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: method name is good but I may still choose verifyPersistedData... I leave it to you...Please feel free to ignore this suggestion...

@@ -68,6 +68,36 @@ describe('cloudant connector', function() {
});
};
});
it('should create new instance if id provided', function(done) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to ignore since in this file other tests use the same pattern; however, in general we use imperative mood, we do not start test names with should. Please see: https://github.com/strongloop/loopback.io/blob/gh-pages/pages/en/contrib/style-guide.md#test-naming

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may use 'Primary key' instead of id; also you could add a test when the Primary key is not called id as well...

if (err) return cb(err);
dataExist = cnt === 1 ? true : false;

if (id && dataExist === true) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

if (id && dataExist) {
   ...
}

cb(err, self.fromDB(model, mo, doc), {isNewInstance: !rev});
mo.db.get(id, function(err, doc) {
if (err) return cb(err);
cb(err, self.fromDB(model, mo, doc), {isNewInstance: !rev});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in this branch rev always exists and !rev would always be false, right? In this case wouldn't it be more clear if you pass {isNewInstance: false} instead? Is my understanding right here? Correct me if I'm wrong.

self._insert(model, data, function(err) {

// Check if data exist first to avoid
// running getCurrentRevision() again an nonexistent instance
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if data exists to avoid running 'getCurrentRevision()' against new data.

// Check if data exist first to avoid
// running getCurrentRevision() again an nonexistent instance
var where = {}; where[idName] = id;
var dataExist;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var where = {};
where[idName] = id;
var dataExist;

});
}
};
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not that much familiar with cloudant; is this the only way to do it in cloudant? Isn't there any atomic method to use? I trust your judgment in this regard...

@bajtos
Copy link
Member

bajtos commented Nov 9, 2016

If I understand this change correctly, then each replaceOrCreate operation is making three database calls now: count, getRevision, replaceById or create. That creates a long window where another request may come in and interfere with our operation in progress :( Is this implementation any better than using the naive algorithm from juggler (findById + replaceById or create)? If my understanding is correct, then neither of these implementations is truly atomic.

When I step back a bit and look at the problem from a high level perspective, then I think it should be acceptable to require callers of patch/replace operations to provide _rev in the data, as that's the way how CouchDB/Cloudant allows clients to detect and resolve conflicts. With this requirement in place, the implementation in this connector should be much simpler - no need to call getRevision, just call _insert directly.

Thoughts?

@jannyHou
Copy link
Contributor Author

jannyHou commented Nov 14, 2016

@bajtos

to require callers of patch/replace operations to provide _rev in the data

A block of this idea is, user cannot get rev by find() or findById() since method fromDB deletes it:
https://github.com/strongloop/loopback-connector-cloudant/blob/master/lib/cloudant.js#L126-L127

My suggestion is adding a config option includeRev or simply rev when calling fromDB():

  • if true, returns rev of data, don't delete it
  • if false, delete it as our current implementation. And make default as false

Since getCurrentRevision() is also applied in other functions, I opened a separate issue to discuss it: #55


On my second thought, I think

That creates a long window where another request may come in and interfere with our operation in progress :(

would not be a problem in cloudant, see cloudant doc for transactions.
So ideally with a well designed app, it won't work in a way that different users frequently send request against one single doc.

@Amir-61 Amir-61 removed their assignment Nov 25, 2016
@bajtos bajtos removed their assignment Dec 1, 2016
@jannyHou
Copy link
Contributor Author

jannyHou commented Dec 8, 2016

Closing this pr since we agree on #55

@jannyHou jannyHou closed this Dec 8, 2016
@jannyHou jannyHou removed the #review label Dec 8, 2016
@jannyHou jannyHou deleted the fix/create-with-id branch February 1, 2017 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants