0

I'm having a problem trying, what boils down to, incrementing a field in a document or inserting an entire document. The context is "trying to insert an initial document for a sequence or incrementing the sequence number for an existing sequence".

This code:

private async Task<int> GetSequenceNumber(string sequenceName)
{
    var filter = new ExpressionFilterDefinition<Sequence>(x => x.Id == sequenceName);
    var builder = Builders<Sequence>.Update;
    var update = builder
        .SetOnInsert(x => x.CurrentValue, 1000)
        .Inc(x => x.CurrentValue, 1);

    var sequence = await _context.SequenceNumbers.FindOneAndUpdateAsync(
        filter, 
        update, 
        new FindOneAndUpdateOptions<Sequence>
        {
            IsUpsert = true, 
            ReturnDocument = ReturnDocument.After,
        });

    return sequence.CurrentValue;
}

results in the exception

MongoDB.Driver.MongoCommandException: Command findAndModify failed: Updating the path 'currentvalue' would create a conflict at 'currentvalue'. at MongoDB.Driver.Core.WireProtocol.CommandUsingCommandMessageWireProtocol`1.ProcessResponse(ConnectionId connectionId, CommandMessage responseMessage)

Removing the SetOnInsert results in no errors, but inserts a document with the currentValue equal to 1 instead of the expected 1000.

It almost appears if SetOnInsert is not being honored, and that what's happening is a default document is inserted and then currentValue is incremented via Inc atomically as the new document is created.

How do I overcome these issues? A non-C# solution would also be welcome, as I could translate that...

Kit
  • 20,354
  • 4
  • 60
  • 103
  • it's a server restriction, see here https://stackoverflow.com/questions/50947772/updating-the-path-x-would-create-a-conflict-at-x – dododo Nov 04 '20 at 16:58

1 Answers1

0

Ok thanks to @dododo in the comments, I now realize that both an Inc and a SetOnInsert can't be applied at the same time. It's unintuitive because you'd think the former would apply on update only and the latter on insert only.

I went with the solution below, which suffers more than one round-trip, but at least works, and appears to work with my concurrency based tests.

public async Task<int> GetSequenceNumber(string sequenceName, int tryCount)
{
    if (tryCount > 5) throw new InvalidOperationException();

    var filter = new ExpressionFilterDefinition<Sequence>(x => x.Id == sequenceName);
    var builder = Builders<Sequence>.Update;

    // optimistically assume value was already initialized
    var update = builder.Inc(x => x.CurrentValue, 1);

    var sequence = await _context.SequenceNumbers.FindOneAndUpdateAsync(
        filter, 
        update, 
        new FindOneAndUpdateOptions<Sequence>
        {
            IsUpsert = true, 
            ReturnDocument = ReturnDocument.After,
        });

    if (sequence == null)
        try
        {
            // we have to try to save a new sequence...
            sequence = new Sequence { Id = sequenceName, CurrentValue = 1001 };
            await _context.SequenceNumbers.InsertOneAsync(sequence);
        }
        // ...but something else could beat us to it
        catch (MongoWriteException e) when (e.WriteError.Code == DuplicateKeyCode)
        {
            // ...so we have to retry an update
            return await GetSequenceNumber(sequenceName, tryCount + 1);
        }

    return sequence.CurrentValue;
}

I'm sure there are other options. It may be possible to use an aggregation pipeline, for example.

Kit
  • 20,354
  • 4
  • 60
  • 103