Example #1
0
        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);
                    }
                });
            }
        }