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 }); } }