예제 #1
0
        public static void Log(Command c, string columnName, int partition)
        {
            var colId = String.Format("{0,15}", $"[{columnName}|{partition}]");

            var summary = c switch
            {
                Command_Change cmd => $"{colId} CHANGE  {cmd.ColumnValue} | {cmd.ChangeType}",
                Command_Enter cmd => $"{colId} ENTER   {cmd.ColumnValue}",
                Command_Verify cmd => $"{colId} VERIFY  {cmd.SourceColumnName} | {cmd.SourceColumnValue}",
                Command_Exit cmd => $"{colId} EXIT    {cmd.ColumnValue}",
                Command_Ack cmd => $"{colId} ACK     {cmd.SourceColumnName} | {cmd.SourceColumnValue}",
                _ => $"unknown command"
            };

            var logLine = String.Format("{0,-60} .." + c.Correlation.Substring(c.Correlation.Length - 8), summary);

            Console.WriteLine(logLine);
        }
        private void HandleVerify(Command_Verify cmd, Offset offset)
        {
            if (!inProgressState_Active.ContainsKey(cmd.Correlation))
            {
                // This could occur in the case of duplicate writes and can be safely ignored.
                Logger.Log("VERIFY", $"Received verify command with no corresponding waitingVerify entry, ignoring [{cmd.Correlation}]");
                return;
            }

            // all verify results must be true for command to succeed.
            if (!cmd.Verified)
            {
                inProgressState_Active[cmd.Correlation].VerifyFailed.Add(new NameAndValue {
                    Name = cmd.SourceColumnName, Value = cmd.SourceColumnValue
                });
            }

            // remove the received column from list we're waiting on.
            inProgressState_Active[cmd.Correlation].WaitingVerify = inProgressState_Active[cmd.Correlation]
                                                                    .WaitingVerify.Where(a => !(a.Name == cmd.SourceColumnName && a.Value == cmd.SourceColumnValue))
                                                                    .ToList();

            // if we're waiting on more, then there's nothing left to do here.
            if (inProgressState_Active[cmd.Correlation].WaitingVerify.Count > 0)
            {
                return;
            }


            // --- verification result has been received for all columns.
            // Logger.Log("VERIFY", $"All verify commands received for: [{cmd.Correlation}]");

            var inProgress = inProgressState_Active[cmd.Correlation];
            var aborting   = inProgress.VerifyFailed.Count > 0;

            // send exit_commands for columns that are to be set (or abort).
            // note: does not include the current "this" column - that is handled as a special case.
            foreach (var col in inProgress.ToSet)
            {
                Dictionary <string, string> dataToSet = null;
                if (!aborting)
                {
                    dataToSet = new Dictionary <string, string>(inProgress.Data);
                    dataToSet.Add(this.columnName, inProgress.ColumnValue);
                    dataToSet.Remove(col.Key);
                }

                var exitCommand = new Command_Exit
                {
                    Correlation       = cmd.Correlation,
                    ColumnValue       = inProgress.Data[col.Key],
                    Action            = aborting ? ActionType.Abort : ActionType.Set,
                    Data              = dataToSet,
                    SourceColumnName  = this.columnName,
                    SourceColumnValue = inProgress.ColumnValue
                };

                var tp = new TopicPartition(
                    tableSpecification.CommandTopicName(col.Key),
                    Table.Partitioner(exitCommand.ColumnValue, this.numPartitions));

                cmdProducer.ProduceAsync(tp, new Message <Null, string> {
                    Value = JsonConvert.SerializeObject(exitCommand, Formatting.Indented, jsonSettings)
                })
                .FailIfFaulted("VERIFY", "produce fail");
            }

            // send exit_commands for column values that are to be deleted (or abort).
            foreach (var col in inProgress.ToDelete)
            {
                var exitCommand = new Command_Exit
                {
                    Correlation       = cmd.Correlation,
                    ColumnValue       = col.Value,
                    Action            = aborting ? ActionType.Abort : ActionType.Delete,
                    Data              = null,
                    SourceColumnName  = this.columnName,
                    SourceColumnValue = inProgress.ColumnValue
                };

                var tp = new TopicPartition(
                    tableSpecification.CommandTopicName(col.Key),
                    Table.Partitioner(exitCommand.ColumnValue, this.numPartitions));

                cmdProducer.ProduceAsync(tp, new Message <Null, string> {
                    Value = JsonConvert.SerializeObject(exitCommand, Formatting.Indented, jsonSettings)
                })
                .FailIfFaulted("VERIFY", "produce fail");
            }

            // if aborting, clean up everything now - there will be no acks.
            if (aborting)
            {
                locked.Remove(inProgress.ColumnValue);
                blockedForCommit.Remove(inProgress.ChangeCommandOffset);
                inProgressState_Active.Remove(cmd.Correlation);
                CompleteChangeRequest(cmd.Correlation, new Exception("columns verify failed"));
                return;
            }
        }
        private void HandleEnter(Command_Enter cmd, Offset offset)
        {
            bool canChange = true;

            // deal with case where value is locked already.
            if (locked.ContainsKey(cmd.ColumnValue))
            {
                if (locked[cmd.ColumnValue] != cmd.Correlation) // dedupe.
                {
                    Logger.Log("ENTER", $"Duplicate lock command received, ignoring [{cmd.Correlation}]");
                    return;
                }
                else
                {
                    Logger.Log("ENTER", $"Attempting to lock key that is already locked: blocking the change operation [{cmd.Correlation}]");
                    canChange = false;
                }
            }

            // deal with case where value is already materialized.
            if (materialized.ContainsKey(cmd.ColumnValue))
            {
                if (cmd.IsAddCommand)
                {
                    Logger.Log("ENTER", $"Attempting to add a new row with unique column '{this.columnName}' value '{cmd.ColumnValue}' that already exists. [{cmd.Correlation}]");
                    canChange = false;
                }

                if (!cmd.IsAddCommand)
                {
                    // in the case of an update, it is fine for the key to exist, if the row corresponds to the one being updated.
                    if (materialized[cmd.ColumnValue][cmd.SourceColumnName] != cmd.SourceColumnValue)
                    {
                        Logger.Log("ENTER", $"Attempting to update a row with unique column '{this.columnName}' value '{cmd.ColumnValue}' that already exists for some other row. [{cmd.Correlation}]");
                        canChange = false;
                    }
                }
            }

            var verifyCommand = new Command_Verify
            {
                Correlation       = cmd.Correlation,
                Verified          = canChange,
                SourceColumnName  = this.columnName,
                SourceColumnValue = cmd.ColumnValue
            };

            var tp = new TopicPartition(
                tableSpecification.CommandTopicName(cmd.SourceColumnName),
                Table.Partitioner(cmd.SourceColumnValue, numPartitions));

            cmdProducer.ProduceAsync(tp, new Message <Null, string> {
                Value = JsonConvert.SerializeObject(verifyCommand, Formatting.Indented, jsonSettings)
            })
            .FailIfFaulted("ENTER", $"A fatal problem occured writing a verify command.");

            // locking is only required if the value may possibly be changing.
            if (canChange)
            {
                // lock the value until the command is complete [this may not be necessary].
                locked.Add(cmd.ColumnValue, cmd.Correlation);

                // don't allow commit until corresponding exit command
                blockedForCommit.Add(offset, cmd.Correlation);

                // remember the offset, to remove from blockedForCommit on exit.
                inProgressState_Secondary.Add(cmd.Correlation, new InProgressState_Secondary {
                    EnterCommandOffset = offset
                });
            }
        }