public void ExecuteTransaction(IList <CachedObject> itemsToPut, IList <OrQuery> conditions, IList <CachedObject> itemsToDelete = null) { if (itemsToPut.Count != conditions.Count) { throw new ArgumentException($"{nameof(itemsToPut)} and {nameof(conditions)} do not have the same size"); } // the same connector will not execute transactions in parallel lock (_transactionSync) { // split the global transaction between servers var itemsByServer = new ConcurrentDictionary <int, TransactionRequest>(); var transactionId = TransactionRequest.GenerateId(); var index = 0; foreach (var item in itemsToPut) { var serverIndex = WhichNode(item); if (!itemsByServer.ContainsKey(serverIndex)) { itemsByServer.TryAdd(serverIndex, new TransactionRequest()); } itemsByServer[serverIndex].ItemsToPut.Add(item); itemsByServer[serverIndex].Conditions.Add(conditions[index]); itemsByServer[serverIndex].TransactionId = transactionId; index++; } if (itemsToDelete != null) { foreach (var item in itemsToDelete) { var serverIndex = WhichNode(item); if (!itemsByServer.ContainsKey(serverIndex)) { itemsByServer.TryAdd(serverIndex, new TransactionRequest()); } itemsByServer[serverIndex].ItemsToDelete.Add(item); index++; } } // Fallback to single stage if only one node is concerned if (itemsByServer.Count == 1) { var server = itemsByServer.Keys.Single(); CacheClients[server].ExecuteTransaction(itemsToPut, conditions, itemsToDelete); TransactionStatistics.ExecutedAsSingleStage(); return; } var sessions = new ConcurrentDictionary <int, Session>(); var serverStatus = new ConcurrentDictionary <int, bool>(); // select only the clients that are concerned var clients = CacheClients.Where(c => itemsByServer.ContainsKey(c.ShardIndex)).ToList(); // first stage : send the transaction request to the servers and wait for them to acquire write locks SendRequestsAndWaitForLock(clients, itemsByServer, sessions, transactionId, serverStatus); Dbg.Trace($"C: proceeding with first stage transaction {transactionId} "); var exType = ExceptionType.Unknown; // first stage: the durable transaction is written in the transaction log Parallel.ForEach(clients, client => { try { var session = sessions[client.ShardIndex]; var response = client.Channel.GetResponse(session); if (response is ReadyResponse) { serverStatus[client.ShardIndex] = true; } else { serverStatus[client.ShardIndex] = false; if (response is ExceptionResponse exceptionResponse) { exType = exceptionResponse.ExceptionType; } } } catch (Exception) { serverStatus[client.ShardIndex] = false; } }); // second stage: commit or rollback only the servers that processed successfully the first stage // (if a server answered with an exception response the transaction was already rolled back on this server) var firstStageOk = serverStatus.Values.All(s => s); if (firstStageOk) { // commit the transaction Dbg.Trace($"C: proceeding with second stage transaction {transactionId} "); Parallel.ForEach(clients, client => { var session = sessions[client.ShardIndex]; client.Channel.Continue(session, true); }); TransactionStatistics.NewTransactionCompleted(); } else { Dbg.Trace($"C: rollback first stage transaction {transactionId} "); Parallel.ForEach(clients, client => { // need to rollback only the clients that have executed the first stage if (serverStatus[client.ShardIndex]) { var session = sessions[client.ShardIndex]; client.Channel.Continue(session, false); } }); throw new CacheException( $"Error in two stage transaction. The transaction was successfully rolled back: {exType}", exType); } // close the session Parallel.ForEach(CacheClients, client => { if (itemsByServer.ContainsKey(client.ShardIndex)) { var session = sessions[client.ShardIndex]; client.Channel.EndSession(session); } }); } }