private async Task <MongoDbTransaction> GetLastTransactionInternal() { // look for potential last transaction var res = await TransactionCollection.Find(x => true) .SortByDescending(x => x.Timestamp) .FirstOrDefaultAsync(); if (res == null) { return(null); } // look if a pending transaction var resp = await PendingTransactionCollection.Find(x => true).SortByDescending(x => x.Timestamp).FirstOrDefaultAsync(); if (resp == null) // no pending transaction { // return last transaction return(res); } else { // else return last transaction no younger than pending transaction res = await TransactionCollection.Find(x => x.Timestamp < resp.Timestamp) .SortByDescending(x => x.Timestamp) .FirstOrDefaultAsync(); return(res); } }
public async Task RollbackAllPendingTransactions(DateTime limit) { var res = await PendingTransactionCollection.Find(x => x.LockTimestamp < limit).ToListAsync(); foreach (var t in res) { await RollbackTransaction(t.TransactionHash); } }
private async Task RollbackTransaction(byte[] hash) { // Rollback is idempotent && reentrant : may be call twice even at the same time try { #if DEBUG Logger.LogDebug($"Rollbacking transaction {new ByteString(hash)}"); #endif // get affected records var trn = await PendingTransactionCollection.Find(x => x.TransactionHash.Equals(hash)).SingleOrDefaultAsync(); if (trn != null) { // revert records values & version foreach (var r in trn.InitialRecords) { await RecordCollection.FindOneAndUpdateAsync( x => x.Key.Equals(r.Key) && x.TransactionLock.Equals(trn.LockToken), Builders <MongoDbRecord> .Update.Set(x => x.Value, r.Value).Set(x => x.Version, r.Version).Unset(x => x.TransactionLock) ); } foreach (var r in trn.AddedRecords) { await RecordCollection.FindOneAndDeleteAsync( x => x.Key.Equals(r) && x.TransactionLock.Equals(trn.LockToken) ); } // remove transaction await TransactionCollection.DeleteOneAsync(x => x.TransactionHash.Equals(hash)); await RecordCollection.UpdateOneAsync( x => x.TransactionLock == trn.LockToken, Builders <MongoDbRecord> .Update .Unset(x => x.TransactionLock) ); // remove pending transaction await PendingTransactionCollection.DeleteOneAsync(x => x.TransactionHash.Equals(hash)); Logger.LogInformation($"Transaction {new ByteString(hash)} rollbacked"); } } catch (Exception ex) { var msg = "Error rollbacking transaction : " + new ByteString(hash).ToString(); Logger.LogCritical(msg, ex); throw new Exception(msg, ex); } }
public async Task AddTransactions(IEnumerable <ByteString> transactions) { List <byte[]> transactionHashes = new List <byte[]>(); List <Record> lockedRecords = new List <Record>(); byte[] lockToken = Guid.NewGuid().ToByteArray(); try { foreach (ByteString rawTransaction in transactions) { byte[] rawTransactionBuffer = rawTransaction.ToByteArray(); Transaction transaction = MessageSerializer.DeserializeTransaction(rawTransaction); byte[] transactionHash = MessageSerializer.ComputeHash(rawTransactionBuffer); byte[] mutationHash = MessageSerializer.ComputeHash(transaction.Mutation.ToByteArray()); Mutation mutation = MessageSerializer.DeserializeMutation(transaction.Mutation); List <byte[]> records = new List <byte[]>(); #if DEBUG Logger.LogDebug($"Add transaction {new ByteString(transactionHash)} token {new ByteString(lockToken)}"); #endif transactionHashes.Add(transactionHash); // add pending transaction var ptr = new MongoDbPendingTransaction { MutationHash = mutationHash, TransactionHash = transactionHash, RawData = rawTransactionBuffer, LockTimestamp = DateTime.UtcNow, InitialRecords = new List <MongoDbRecord>(), AddedRecords = new List <byte[]>(), LockToken = lockToken }; await PendingTransactionCollection.InsertOneAsync(ptr); // lock records foreach (var r in mutation.Records) { var previous = await LockRecord(lockToken, r); if (previous != null) { ptr.InitialRecords.Add(previous); lockedRecords.Add(r); } else if (r.Value != null) { ptr.AddedRecords.Add(r.Key.ToByteArray()); lockedRecords.Add(r); } } // save original records await PendingTransactionCollection.UpdateOneAsync( x => x.TransactionHash.Equals(transactionHash), Builders <MongoDbPendingTransaction> .Update .Set(x => x.InitialRecords, ptr.InitialRecords) .Set(x => x.AddedRecords, ptr.AddedRecords) ); // update records foreach (var rec in mutation.Records) { MongoDbRecord r = BuildMongoDbRecord(rec); if (r.Value == null) { if (r.Version.Length == 0) // No record expected { var res = await RecordCollection.CountAsync(x => x.Key.Equals(r.Key)); if (res != 0) // a record exists { throw new ConcurrentMutationException(rec); } } else { // specific version expected var res = await RecordCollection.CountAsync(x => x.Key.Equals(r.Key) && x.Version.Equals(r.Version)); if (res != 1) // expected version not found { throw new ConcurrentMutationException(rec); } } } else { if (r.Version.Length == 0) { r.Version = mutationHash; r.TransactionLock = lockToken; try { await RecordCollection.InsertOneAsync(r); } catch (MongoWriteException ex) when(ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { throw new ConcurrentMutationException(rec); } } else { var res = await RecordCollection.UpdateOneAsync( x => x.Key.Equals(r.Key) && x.Version.Equals(r.Version) && x.TransactionLock.Equals(lockToken), Builders <MongoDbRecord> .Update .Set(x => x.Value, r.Value) .Set(x => x.Version, mutationHash) ); if (res.MatchedCount != 1 || res.ModifiedCount != 1) { throw new ConcurrentMutationException(rec); } } records.Add(r.Key); } } // add transaction var tr = new MongoDbTransaction { MutationHash = mutationHash, TransactionHash = transactionHash, RawData = rawTransactionBuffer, Records = records }; await TransactionCollection.InsertOneAsync(tr); } // unlock records List <ByteString> l = new List <ByteString>(); foreach (var r in lockedRecords) { if (!l.Contains(r.Key)) { await UnlockRecord(lockToken, r); l.Add(r.Key); } } // remove pending transaction foreach (var hash in transactionHashes) { await PendingTransactionCollection.DeleteOneAsync(x => x.TransactionHash.Equals(hash)); } #if DEBUG Logger.LogDebug($"Transaction committed token {new ByteString(lockToken)}"); #endif } catch (Exception ex1) { #if DEBUG Logger.LogDebug($"Error committing transaction batch {ex1.Message} token {new ByteString(lockToken)}"); #endif foreach (var hash in transactionHashes) { #if DEBUG Logger.LogDebug($"Rollbacking transaction 0 {new ByteString(hash)}"); #endif try { await RollbackTransaction(hash); } catch (Exception ex2) { throw new AggregateException(ex2, ex1); } } throw; } }