Skip to content

Inconsistent cycle detection when saving multiple objects causing RuntimeException #1075

@shlusiak

Description

@shlusiak

Parse version: 1.25.0

java.lang.RuntimeException: Unable to save a ParseObject with a relation to a cycle.
        at com.parse.ParseObject$12.then(ParseObject.java:733)
        at com.parse.ParseObject$12.then(ParseObject.java:713)
        at com.parse.boltsinternal.Task$15.run(Task.java:907)
        at com.parse.boltsinternal.BoltsExecutors$ImmediateExecutor.execute(BoltsExecutors.java:113)
        at com.parse.boltsinternal.Task.completeAfterTask(Task.java:898)
        at com.parse.boltsinternal.Task.continueWithTask(Task.java:713)
        at com.parse.boltsinternal.Task.continueWithTask(Task.java:724)
        at com.parse.boltsinternal.Task$13.then(Task.java:816)
        at com.parse.boltsinternal.Task$13.then(Task.java:804)
        at com.parse.boltsinternal.Task$15.run(Task.java:907)
        at com.parse.boltsinternal.BoltsExecutors$ImmediateExecutor.execute(BoltsExecutors.java:113)

Where this is thrown there is a comment that this should never actually happen:

if (current.size() == 0 && filesComplete.get() && usersComplete.get()) {
// We do cycle-detection when building the list of objects passed to this function, so
// this should never get called. But we should check for it anyway, so that we get an
// exception instead of an infinite loop.
throw new RuntimeException("Unable to save a ParseObject with a relation to a cycle.");

Sample test case that triggers this

val parent = ParseObject("parent")
parent.save()
// parent now has an objectId

val child1 = ParseObject("child")
child1.put("parent", parent)

val child2 = ParseObject("child")
child2.put("parent", parent)

// both children are new and point to the parent, which points back at each.
// this is a cycle, but not a cycle that cannot be resolved by saving the objects in two batches
parent.put("children", listOf(child1, child2)
parent.save()

Expected behaviour

This should succeed, because child1 and child2 can be persisted first, and once they both have an objectId parent can be persisted to resolve the cycle.

Actual behaviour

java.lang.RuntimeException: Unable to save a ParseObject with a relation to a cycle.

This setup does contain a cycle but one that can be resolved because the parent has an objectId. The logic in collectDirtyChildren only looks for direct cycles involving unsaved objects, which cannot be resolved because neither object can be saved first.

In the scenario above the batching code would be expected to serialise and save child1 and child2 in the first iteration, and then parent in the second iteration, once the objectIds of all children are known.

However the canBeSerialized() method is using ParseTraverser, which does a deep traverse of the given object.

// This method is only used for batching sets of objects for saveAll
// and when saving children automatically. Since it's only used to
// determine whether or not save should be called on them, it only
// needs to examine their current values, so we use estimatedData.
new ParseTraverser() {
@Override
protected boolean visit(Object value) {
if (value instanceof ParseFile) {
ParseFile file = (ParseFile) value;
if (file.isDirty()) {
result.set(false);
}
}
if (value instanceof ParseObject) {
ParseObject object = (ParseObject) value;
if (object.getObjectId() == null) {
result.set(false);
}
}
// Continue to traverse only if it can still be serialized.
return result.get();
}
}.setYieldRoot(false).setTraverseParseObjects(true).traverse(this);

This will return false if any node is found that has no objectId, which makes every object in the above scenario un-serializable, because they all contain a node in the tree without an objectId (parent cannot be saved, because child1 is found, child1 cannot be saved because child2 is found, child2 cannot be saved because child1 is found).

I feel what we'd want here is to set setTraverseParseObjects to false to only traverse this object but not do a deep traversal, but the way the ParseTraverser is written this means that ParseObjects will not even be visited. 🙄

Metadata

Metadata

Assignees

No one assigned

    Labels

    type:bugImpaired feature or lacking behavior that is likely assumed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions