In Gremlin, how do I modify a vertex's properties only if a version property matches a number?

2k Views Asked by At

In Gremlin/Tinkerpop, I want to perform a versioned upsert on a Vertex. If there is an existing Vertex, I only want to mutate it if the version matches a version number property.

Below is the code that attempts to do this using gremlinjs. It fails to create the vertex, a later query is unable to find it.

(the prior version of this had a compilation error, but that was a javscriptism undocumented syntax issue)

[UPDATE] see comments in answer as to what was wrong. Working version at https://gist.github.com/pszabop/3b07fa7caadf1dbd86953a713ed96ce0

//
// o.id o.__version, and o.__lastUpdate have special meaning or are reserved
//
graphdb.upsertVertexVersioned = async function(type, o) {
  const g = traversal().withRemote(this.connection);
  let oldVersion;

  if (!o.id) {
    o.id = uuidv4();
  }  
  if (!o.__version) {
    o.__version = 0;
    oldVersion = 0;
  } else {
    oldVersion = o.__version;
    o.__version++;
  }  
  o.__lastUpdate = Date.now();

  // @see http://tinkerpop.apache.org/docs/current/recipes/#element-existence
  // The pattern we are using is keys get copied into properties that can be used
  // by the graph database for its work, and then the
  // entire object is JSON serialized into a generic `obj` property.
  // XXX TBD use graphson?
  const v1 = await g.V().has(type, 'id', o.id) 
  .fold()  
  .coalesce(__.unfold(),
    __.addV(type).property('id', o.id)                       
    .property('version', o.__version)                        
  ).choose(__.values('version').is(oldVersion),
    __.in_() 
    .property('lastUpdate', o.__lastUpdate)                  // updated properties go here
    .property('version', o.__version)                        // updated properties go here
    .property('obj', JSON.stringify(o)),                     // updated properties go here
    __.out()
  ).next();

  return o;
};

References:

Versions

  • janusgraph/janusgraph:latest
  • gremlinjs 3.4.4
2

There are 2 best solutions below

2
On

I tried a variation of your code using the "modern" toy graph and your code proved out to be right to me. I believe the following captures the spirit of what you were doing:

gremlin> g = TinkerFactory.createModern().traversal()
==>graphtraversalsource[tinkergraph[vertices:6 edges:6], standard]
gremlin> g.V().property('version',1).iterate()
gremlin> name = 'marko'
==>marko
gremlin> oldVersion = 1
==>1
gremlin> version = 2
==>2
gremlin> g.V().has('person','name',name).
......1>   fold().
......2>   coalesce(unfold(), 
......3>            addV('person').property('name',name).property('version',version)).
......4>   choose(values('version').is(oldVersion),
......5>          property('version', version).constant('edited'),
......6>          constant('same'))
==>edited
gremlin> g.V().has('person','name',name).
......1>   fold().
......2>   coalesce(unfold(), 
......3>            addV('person').property('name',name).property('version',version)).
......4>   choose(values('version').is(oldVersion),
......5>          property('version', version).constant('edited'),
......6>          constant('same'))
==>same
gremlin> name = 'stephen'
==>stephen
gremlin> g.V().has('person','name',name).
......1>   fold().
......2>   coalesce(unfold(), 
......3>            addV('person').property('name',name).property('version',version)).
......4>   choose(values('version').is(oldVersion),
......5>          property('version', version).constant('edited'),
......6>          constant('same'))
==>same
gremlin> g.V().has('person','name',name).
......1>   fold().
......2>   coalesce(unfold(), 
......3>            addV('person').property('name',name).property('version',version)).
......4>   choose(values('version').is(oldVersion),
......5>          property('version', version).constant('edited'),
......6>          constant('same'))
==>same
gremlin> oldVersion = 2
==>2
gremlin> version = 3
==>3
gremlin> 
gremlin> g.V().has('person','name',name).
......1>   fold().
......2>   coalesce(unfold(), 
......3>            addV('person').property('name',name).property('version',version)).
......4>   choose(values('version').is(oldVersion),
......5>          property('version', version).constant('edited'),
......6>          constant('same'))
==>edited
gremlin> g.V().has('person','name',name).
......1>   fold().
......2>   coalesce(unfold(), 
......3>            addV('person').property('name',name).property('version',version)).
......4>   choose(values('version').is(oldVersion),
......5>          property('version', version).constant('edited'),
......6>          constant('same'))
==>same
gremlin> g.V().has('person','name','stephen').elementMap()
==>[id:19,label:person,name:stephen,version:3]

I would suggest trying to simplify a bit given your description of the problem. You stated the problem was that:

It fails to create the vertex, a later query is unable to find it.

Does it work properly if you remove the choose() logic? In other words, can you get the basic upsert operation to work? If not, then the problem seems isolated to that part of the query, though what you have seems to follow recommended practices, so I'm not sure what might be amiss.

0
On

The code below in gremlinjs updates a vertex only if the version number property matches. This allows a safe read-modify-write of a vertex using optimistic concurrency (i.e. collisions should be rare or you should do something else)

Code is available as a gist: https://gist.github.com/pszabop/3b07fa7caadf1dbd86953a713ed96ce0

//
// o.id o.__version, and o.__lastUpdate have special meaning or are reserved
//
graphdb.upsertVertexVersioned = async function(type, o) {
  const g = traversal().withRemote(this.connection);
  let oldVersion;

  // don't modify original in case of exceptions
  // return the new object and let user decide to reassign or not
  o = Object.assign({}, o);

  if (!o.id) {
    o.id = uuidv4();
  }
  if (!Number.isInteger(o.__version)) {
    o.__version = 0;
    oldVersion = 0;
  } else {
    oldVersion = o.__version;
    o.__version++;
  }
  o.__lastUpdate = Date.now();

  // @see http://tinkerpop.apache.org/docs/current/recipes/#element-existence
  // @see https://stackoverflow.com/questions/58513680/in-gremlin-how-do-i-modify-a-vertexs-properties-only-if-a-version-property-mat
  // The pattern we are using is keys get copied into properties that can be used
  // by the graph database for its work, and then the
  // entire object is JSON serialized into a generic `obj` property.
  // XXX TBD use graphson?
  const v1 = await g.V().has(type, 'id', o.id)
  .fold()
  .coalesce(__.unfold(),
    __.addV(type).property('id', o.id)
    .property('version', o.__version)
  ).choose(__.values('version').is(oldVersion),
    __.property('lastUpdate', o.__lastUpdate)                  // updated properties go here
    .property('version', o.__version)
    .property('obj', JSON.stringify(o)).constant('edited'),
    __.constant('unchanged')
  ).next();

  if (v1.value === 'unchanged') {
    throw new Error('version mismatch, vertex not updated');
  }

  return o;
};

test('test vertex versioned upsert and get', async function(t) {

  graphdb.open();

  // initial write and verify
  const o = { randomText: uuidv4(), foo: 'bar'}
  const osent1 = await graphdb.upsertVertexVersioned('testtype', o);
  t.ok(osent1.id, 'a random ID was assigned');
  const oget1 = await graphdb.getVertex('testtype', osent1.id);
  t.equal(oget1.randomText, o.randomText, 'random text was as written');
  t.equal(oget1.id, osent1.id, 'ID was as assigned');
  t.equal(oget1.foo, 'bar', 'field foo is "bar"');

  // make sure version gets updated when field foo is modified
  oget1.foo = 'beyond all repair';
  const osent2 = await graphdb.upsertVertexVersioned('testtype', oget1);
  t.equal(osent2.__version, 1, 'version was changed from 0 to 1');
  const oget2 = await graphdb.getVertex('testtype', oget1.id);
  t.equal(oget2.randomText, o.randomText, 'random text was as written and was unchanged on second write');
  t.equal(oget2.id, osent1.id, 'ID was as assigned');
  t.equal(oget2.foo, 'beyond all repair', 'field foo was changed to "beyond all repair"');

  // if we are using a stale copy of the object an update should not happen
  osent1.foo = 'illegal update';
  try {
    const osent3 = await graphdb.upsertVertexVersioned('testtype', osent1);
    t.fail('should never returned from an incorrect version update');
  } catch (err) {
    t.ok(err.toString().includes('not updated'), 'error message is correct on illegal version update attempt');
  }
  const oget3 = await graphdb.getVertex('testtype', oget1.id);
  t.equal(oget3.randomText, o.randomText, 'random text was as written and was unchanged on second write');
  t.equal(oget3.id, osent1.id, 'ID was as assigned');
  t.equal(oget3.foo, 'beyond all repair', 'field foo was unchanged after failed update');
  graphdb.close();

});