public async Task ConcurentAutoNrGenerationForTheSameDocumentWorksAsExpectedWhenTwoServicesAsksForNumber() { var tableName = RandomTableNameName(); Console.WriteLine($"Table name for this test: {tableName}"); var service1 = new TableStorageAutoNrService(connString, tableName); var service2 = new TableStorageAutoNrService(connString, tableName); var service3 = new TableStorageAutoNrService(connString, tableName); service1.Init(); service2.Init(); service3.Init(); var offer1 = new TestOffer() { Date = new DateTime(2015, 11, 21) }; var offer2 = new TestOffer() { Date = new DateTime(2015, 11, 22) }; var generator = new AutoNrGenerator <SequenceData, NrData>((sequenceNewNr, config, prev) => { var sequence = $"OFF-{offer2.Date.Year}/{offer2.Date.Month}/"; var nr = $"{sequence}{sequenceNewNr:d5}"; return(new AutoNrResult <SequenceData, NrData>( config ?? new SequenceData(), new NrData() { DocumentNr = nr })); }); var nr1 = await service1.GetAutoNr <SequenceData, NrData>( "rcsoffers", offer1.Id.Value, generator); NrData nr2 = null; var nr3 = await service3.GetAutoNr <SequenceData, NrData>( "rcsoffers", offer2.Id.Value, (sequenceNewNr, config, prev) => { nr2 = service2.GetAutoNr <SequenceData, NrData>("rcsoffers", offer2.Id.Value, generator).Result; return(generator(sequenceNewNr, config, prev)); }); Assert.AreEqual("OFF-2015/11/00001", nr1.DocumentNr); Assert.AreEqual(nr2.DocumentNr, nr3.DocumentNr); Assert.AreEqual("OFF-2015/11/00002", nr2.DocumentNr); }
public async Task GetAutoNrGeneratesUniqueNrsTest() { var tableName = RandomTableNameName(); Console.WriteLine($"Table name for this test: {tableName}"); var service = new TableStorageAutoNrService(connString, tableName); service.Init(); var offers = Enumerable.Range(1, 30).Select(day => new TestOffer() { Date = new DateTime(2015, 11, day) }).ToArray(); var nrs = new NrData[30]; for (int i = 0; i < 30; i++) { var offer = offers[i]; var generator = new AutoNrGenerator <SequenceData, NrData>((sequenceNewNr, config, prev) => { var sequence = $"OFF-{offer.Date.Year}/{offer.Date.Month}/"; var nr = $"{sequence}{sequenceNewNr:d5}"; return(new AutoNrResult <SequenceData, NrData>( config ?? new SequenceData(), new NrData() { DocumentNr = nr })); }); nrs[i] = await service.GetAutoNr("rcsoffers", offer.Id.Value, generator); //Console.WriteLine(nrs[i]); } for (int i = 0; i < 30; i++) { Assert.AreEqual($"OFF-2015/11/{i + 1:d5}", nrs[i].DocumentNr); } }
public async Task <TAutoNrData> GetAutoNr <TSequenceData, TAutoNrData>(string sequence, string aggregateId, AutoNrGenerator <TSequenceData, TAutoNrData> generator) where TSequenceData : class where TAutoNrData : class { return(await GetAutoNrWithRetries(sequence, aggregateId, generator, MaxRetries)); }
private async Task <TAutoNrData> GetAutoNrWithRetries <TSequenceData, TAutoNrData>(string sequence, string aggregateId, AutoNrGenerator <TSequenceData, TAutoNrData> generator, int retries) where TSequenceData : class where TAutoNrData : class { var sequenceEntity = await GetSequenceRow(sequence); // or null if (sequenceEntity == null) { // No sequence row var newSequenceResult = generator(1L, null, null); var firstBatch = new TableBatchOperation(); firstBatch.Insert(SequenceEntity.Create(sequence, 1L, newSequenceResult.SequenceData)); // OperationIndex 0 firstBatch.Insert(NrEntity.Create(sequence, aggregateId, 1L, newSequenceResult.NrData)); // OperationIndex 1 firstBatch.Insert(IdEntity.Create(sequence, aggregateId, 1L)); // OperationIndex 2 try { await table.ExecuteBatchAsync(firstBatch); } catch (StorageException storageException) { int operationIndex; string errorCode; if (!TryParseStorageException(storageException, out operationIndex, out errorCode)) { throw; } switch (operationIndex) { case 0: // Insert(SequenceEntity) if (errorCode == "EntityAlreadyExists") { if (retries > 0) { return(await GetAutoNrWithRetries(sequence, aggregateId, generator, retries - 1)); } throw new InvalidOperationException($"Optimistic lock failed because sequence row already exists. Sequence: {sequence}, nr:1."); } break; case 1: // Insert(NrEntity) if (errorCode == "EntityAlreadyExists") { throw new DuplicateDocumentNrException(sequence, 1L); } break; case 2: // Insert(IdEntity) if (errorCode == "EntityAlreadyExists") { var loadedIdRow = await GetIdRow(sequence, aggregateId); var loadedNrRow = await GetNrRow(sequence, loadedIdRow.Nr); return(loadedNrRow.GetData <TAutoNrData>()); } break; default: break; } throw; } return(newSequenceResult.NrData); } // sequence row present var prevNrRow = await GetNrRow(sequence, sequenceEntity.LastNr); sequenceEntity.LastNr++; var result = generator( sequenceEntity.LastNr, sequenceEntity.GetData <TSequenceData>(), prevNrRow != null ? prevNrRow.GetData <TAutoNrData>() : null); sequenceEntity.SetData(result.SequenceData); var batch = new TableBatchOperation(); batch.Add(TableOperation.Replace(sequenceEntity)); // OperationIndex 0 batch.Add(TableOperation.Insert(NrEntity.Create(sequence, aggregateId, sequenceEntity.LastNr, result.NrData))); // OperationIndex 1 batch.Add(TableOperation.Insert(IdEntity.Create(sequence, aggregateId, sequenceEntity.LastNr))); // OperationIndex 2 if (prevNrRow != null) { batch.Add(TableOperation.Merge(prevNrRow)); // OperationIndex 3 } try { await table.ExecuteBatchAsync(batch); } catch (StorageException storageException) { int operationIndex; string errorCode; if (!TryParseStorageException(storageException, out operationIndex, out errorCode)) { throw; } switch (operationIndex) { case 0: // Replace(sequenceEntity) case 3: // Merge(prevNrRow) if (errorCode == "UpdateConditionNotSatisfied") { if (retries > 0) { return(await GetAutoNrWithRetries(sequence, aggregateId, generator, retries - 1)); } string reason = operationIndex == 0 ? "sequence row has changed" : "prev-nr row has changed"; throw new AutoNrOptimisticException($"Optimistic lock failed because {reason}. Sequence: {sequence}, nr:{sequenceEntity.LastNr}."); } break; case 1: // Insert(NrEntity) if (errorCode == "EntityAlreadyExists") { throw new DuplicateDocumentNrException(sequence, sequenceEntity.LastNr); } break; case 2: // Insert(IdEntity) if (errorCode == "EntityAlreadyExists") { var loadedIdRow = await GetIdRow(sequence, aggregateId); var loadedNrRow = await GetNrRow(sequence, loadedIdRow.Nr); return(loadedNrRow.GetData <TAutoNrData>()); } break; default: break; } throw; } return(result.NrData); }
public Task <TNrData> GetAutoNr <TSequenceData, TNrData>(string sequence, string aggregateId, AutoNrGenerator <TSequenceData, TNrData> generator) where TSequenceData : class where TNrData : class { Sequence contextLock = sequences.GetOrAdd(sequence, Sequence.Create <TSequenceData>(null)); lock (contextLock) // only one thread per context { var context = sequences[sequence]; long seqNr; if (context.TryGet(aggregateId, out seqNr)) { return(Task.FromResult(context.GetAutoNrDataByNr <TNrData>(seqNr))); } else { var prev = context.GetAutoNrDataByNr <TNrData>(context.LastSequenceNr); var newSeqNr = context.LastSequenceNr + 1; var result = generator(newSeqNr, context.GetConfig <TSequenceData>(), prev); context.AddNr(aggregateId, newSeqNr, result.NrData); context.SetConfig(result.SequenceData); context.LastSequenceNr = newSeqNr; return(Task.FromResult(result.NrData)); } } }