/// <summary> /// Only for Follower /// Is called from tryCatch and in lock /// </summary> /// <param name="address"></param> /// <param name="data"></param> void ParseStateLogEntrySuggestion(NodeAddress address, byte[] data) { if (this.NodeState != eNodeState.Follower) { return; } StateLogEntrySuggestion suggest = StateLogEntrySuggestion.BiserDecode(data); //data.DeserializeProtobuf<StateLogEntrySuggestion>(); if (this.NodeTerm > suggest.LeaderTerm) //Sending Leader is not Leader anymore { this.NodeStateLog.LeaderSynchronizationIsActive = false; return; } if (this.NodeTerm < suggest.LeaderTerm) { this.NodeTerm = suggest.LeaderTerm; } if (suggest.StateLogEntry.Index <= NodeStateLog.LastCommittedIndex) //Preventing same entry income, can happen if restoration was sent twice (while switch of leaders) { return; //breakpoint don't remove } //Checking if node can accept current suggestion if (suggest.StateLogEntry.PreviousStateLogId > 0) { var sle = this.NodeStateLog.GetEntryByIndexTerm(suggest.StateLogEntry.PreviousStateLogId, suggest.StateLogEntry.PreviousStateLogTerm); if (sle == null) { //We don't have previous to this log and need new index request //VerbosePrint($"{NodeAddress.NodeAddressId}> in sync 1 "); if (entitySettings.InMemoryEntity && entitySettings.InMemoryEntityStartSyncFromLatestEntity && this.NodeStateLog.LastAppliedIndex == 0) { //helps newly starting mode with specific InMemory parameters get only latest command for the entity } else { this.SyncronizeWithLeader(); return; } } } //We can apply new Log Entry from the Leader and answer successfully this.NodeStateLog.AddToLogFollower(suggest); StateLogEntryApplied applied = new StateLogEntryApplied() { StateLogEntryId = suggest.StateLogEntry.Index, StateLogEntryTerm = suggest.StateLogEntry.Term // RedirectId = suggest.StateLogEntry.RedirectId }; //this.NodeStateLog.LeaderSynchronizationIsActive = false; this.Sender.SendTo(address, eRaftSignalType.StateLogEntryAccepted, applied.SerializeBiser(), this.NodeAddress, entitySettings.EntityName); }
public static StateLogEntrySuggestion BiserDecode(byte[] enc = null, Biser.Decoder extDecoder = null) //!!!!!!!!!!!!!! change return type { Biser.Decoder decoder = null; if (extDecoder == null) { if (enc == null || enc.Length == 0) { return(null); } decoder = new Biser.Decoder(enc); if (decoder.CheckNull()) { return(null); } } else { if (extDecoder.CheckNull()) { return(null); } else { decoder = extDecoder; } } StateLogEntrySuggestion m = new StateLogEntrySuggestion(); //!!!!!!!!!!!!!! change return type m.LeaderTerm = decoder.GetULong(); m.StateLogEntry = StateLogEntry.BiserDecode(extDecoder: decoder); m.IsCommitted = decoder.GetBool(); return(m); }
public void AddLogEntry(StateLogEntrySuggestion suggestion) { //Restoring current values PreviousStateLogId = suggestion.StateLogEntry.PreviousStateLogId; PreviousStateLogTerm = suggestion.StateLogEntry.PreviousStateLogTerm; StateLogId = suggestion.StateLogEntry.Index; StateLogTerm = suggestion.StateLogEntry.Term; using (var t = this.db.GetTransaction()) { t.Insert <byte[], byte[]>(stateTableName, new byte[] { 1 }.ToBytes(suggestion.StateLogEntry.Index, suggestion.StateLogEntry.Term), suggestion.StateLogEntry.SerializeBiser()); t.Commit(); } }
/// <summary> /// Only for Follower /// Is called from tryCatch and in lock /// </summary> /// <param name="address"></param> /// <param name="data"></param> void ParseStateLogEntrySuggestion(NodeRaftAddress address, object data) { if (this.States.NodeState != eNodeState.Follower) { return; } StateLogEntrySuggestion suggest = data as StateLogEntrySuggestion; if (this.NodeTerm > suggest.LeaderTerm) //Sending Leader is not Leader anymore { this.States.LeaderSynchronizationIsActive = false; return; } if (this.NodeTerm < suggest.LeaderTerm) { this.NodeTerm = suggest.LeaderTerm; } if (suggest.StateLogEntry == null || (suggest.StateLogEntry.Index <= NodeStateLog.LastCommittedIndex)) //Preventing same entry income, can happen if restoration was sent twice (while switch of leaders) { return; //breakpoint don't remove } //Checking if node can accept current suggestion if (suggest.StateLogEntry.PreviousStateLogId > 0) { var sle = this.NodeStateLog.GetEntryByIndexTerm(suggest.StateLogEntry.PreviousStateLogId, suggest.StateLogEntry.PreviousStateLogTerm); if (sle == null) { this.SyncronizeWithLeader(); return; } } //We can apply new Log Entry from the Leader and answer successfully this.logHandler.AddLogEntryByFollower(suggest); StateLogEntryApplied applied = new StateLogEntryApplied() { StateLogEntryId = suggest.StateLogEntry.Index, StateLogEntryTerm = suggest.StateLogEntry.Term // RedirectId = suggest.StateLogEntry.RedirectId }; this.network.SendTo(address, eRaftSignalType.StateLogEntryAccepted, applied, this.NodeAddress, entitySettings.EntityName); }
/// <summary> /// /// </summary> /// <param name="suggestion"></param> /// <returns></returns> public void AddToLogFollower(StateLogEntrySuggestion suggestion) { try { if (rn.entitySettings.DelayedPersistenceIsActive) { Tuple <ulong, StateLogEntry> tplSle = null; if ( sleCache.TryGetValue(suggestion.StateLogEntry.Index, out tplSle) && tplSle.Item1 == suggestion.StateLogEntry.Term) { return; //we got it already } } using (var t = this.rn.db.GetTransaction()) { //Removing from the persisted all keys equal or bigger then supplied Log (also from other terms) foreach (var el in t.SelectForwardFromTo <byte[], byte[]>(tblStateLogEntry, new byte[] { 1 }.ToBytes(suggestion.StateLogEntry.Index, ulong.MinValue), true, new byte[] { 1 }.ToBytes(ulong.MaxValue, ulong.MaxValue), true, true)) { if ( el.Key.Substring(1, 8).To_UInt64_BigEndian() == suggestion.StateLogEntry.Index && el.Key.Substring(9, 8).To_UInt64_BigEndian() == suggestion.StateLogEntry.Term ) { //we got it already return; } t.RemoveKey <byte[]>(tblStateLogEntry, el.Key); } if (rn.entitySettings.DelayedPersistenceIsActive) { foreach (var iel in sleCache.Where(r => r.Key >= suggestion.StateLogEntry.Index).Select(r => r.Key).ToList()) { sleCache.Remove(iel); } sleCache[suggestion.StateLogEntry.Index] = new Tuple <ulong, StateLogEntry>(suggestion.StateLogEntry.Term, suggestion.StateLogEntry); } else { t.Insert <byte[], byte[]>(tblStateLogEntry, new byte[] { 1 }.ToBytes(suggestion.StateLogEntry.Index, suggestion.StateLogEntry.Term), suggestion.StateLogEntry.SerializeBiser()); } t.Commit(); } rn.VerbosePrint($"{rn.NodeAddress.NodeAddressId}> AddToLogFollower (I/T): {suggestion.StateLogEntry.Index}/{suggestion.StateLogEntry.Term} -> Result:" + $" { (GetEntryByIndexTerm(suggestion.StateLogEntry.Index, suggestion.StateLogEntry.Term) != null)};"); //Setting new internal LogId PreviousStateLogId = StateLogId; PreviousStateLogTerm = StateLogTerm; StateLogId = suggestion.StateLogEntry.Index; StateLogTerm = suggestion.StateLogEntry.Term; tempPrevStateLogId = PreviousStateLogId; tempPrevStateLogTerm = PreviousStateLogTerm; tempStateLogId = StateLogId; tempStateLogTerm = StateLogTerm; if (suggestion.IsCommitted) { if ( this.LastCommittedIndexTerm > suggestion.StateLogEntry.Term || ( this.LastCommittedIndexTerm == suggestion.StateLogEntry.Term && this.LastCommittedIndex > suggestion.StateLogEntry.Index ) ) { //Should be not possible } else { this.LastCommittedIndex = suggestion.StateLogEntry.Index; this.LastCommittedIndexTerm = suggestion.StateLogEntry.Term; //this.rn.Commited(suggestion.StateLogEntry.Index); this.rn.Commited(); } } } catch (Exception ex) { } if (this.LastCommittedIndex < rn.LeaderHeartbeat.LastStateLogCommittedIndex) { rn.SyncronizeWithLeader(true); } else { LeaderSynchronizationIsActive = false; } }
/// <summary> /// + /// Can be null. /// Must be called inside of operation lock. /// </summary> /// <param name="logEntryId"></param> /// <param name="LeaderTerm"></param> /// <returns></returns> public StateLogEntrySuggestion GetNextStateLogEntrySuggestionFromRequested(StateLogEntryRequest req) { StateLogEntrySuggestion le = new StateLogEntrySuggestion() { LeaderTerm = rn.NodeTerm }; int cnt = 0; StateLogEntry sle = null; ulong prevId = 0; ulong prevTerm = 0; using (var t = this.rn.db.GetTransaction()) { if (req.StateLogEntryId == 0)// && req.StateLogEntryTerm == 0) { if (rn.entitySettings.DelayedPersistenceIsActive && sleCache.Count > 0) { sle = sleCache.OrderBy(r => r.Key).First().Value.Item2; } else { var trow = t.SelectForwardFromTo <byte[], byte[]>(tblStateLogEntry, new byte[] { 1 }.ToBytes(ulong.MinValue, ulong.MinValue), true, new byte[] { 1 }.ToBytes(ulong.MaxValue, ulong.MaxValue), true).FirstOrDefault(); if (trow != null && trow.Exists) { sle = StateLogEntry.BiserDecode(trow.Value); } } //else //{ // //should not normally happen //} if (sle != null) { le.StateLogEntry = sle; if ( LastCommittedIndexTerm >= le.StateLogEntry.Term && LastCommittedIndex >= le.StateLogEntry.Index ) { le.IsCommitted = true; } cnt = 2; } } else { Tuple <ulong, StateLogEntry> sleTpl; bool reForward = true; if (rn.entitySettings.DelayedPersistenceIsActive && sleCache.TryGetValue(req.StateLogEntryId, out sleTpl) ) { cnt++; sle = sleTpl.Item2; prevId = sle.Index; prevTerm = sle.Term; if (sleCache.TryGetValue(req.StateLogEntryId + 1, out sleTpl)) { cnt++; sle = sleTpl.Item2; le.StateLogEntry = sle; } } else { reForward = false; foreach (var el in t.SelectForwardFromTo <byte[], byte[]>(tblStateLogEntry, //new byte[] { 1 }.ToBytes(req.StateLogEntryId, req.StateLogEntryTerm), true, new byte[] { 1 }.ToBytes(req.StateLogEntryId, ulong.MinValue), true, new byte[] { 1 }.ToBytes(ulong.MaxValue, ulong.MaxValue), true).Take(2)) { cnt++; sle = StateLogEntry.BiserDecode(el.Value); if (cnt == 1) { prevId = sle.Index; prevTerm = sle.Term; } else { le.StateLogEntry = sle; } } } if (cnt < 2 && reForward) { ulong toAdd = (ulong)cnt; foreach (var el in t.SelectForwardFromTo <byte[], byte[]>(tblStateLogEntry, //new byte[] { 1 }.ToBytes(req.StateLogEntryId + toAdd, req.StateLogEntryTerm), true, new byte[] { 1 }.ToBytes(req.StateLogEntryId + toAdd, ulong.MinValue), true, new byte[] { 1 }.ToBytes(ulong.MaxValue, ulong.MaxValue), true).Take(2)) { cnt++; sle = StateLogEntry.BiserDecode(el.Value); if (cnt == 1) { prevId = sle.Index; prevTerm = sle.Term; } else { le.StateLogEntry = sle; } } } if (cnt == 2) { le.StateLogEntry.PreviousStateLogId = prevId; le.StateLogEntry.PreviousStateLogTerm = prevTerm; if ( LastCommittedIndexTerm >= le.StateLogEntry.Term && LastCommittedIndex >= le.StateLogEntry.Index ) { le.IsCommitted = true; } } } } //if (first) if (cnt != 2) { return(null); } return(le); }
/// <summary> /// /// </summary> /// <param name="suggestion"></param> /// <returns></returns> public void AddToLogFollower(StateLogEntrySuggestion suggestion) { try { var col = this.db.GetCollection <StateLogEntry>(stateTableName); col.DeleteMany(s => (s.Index > suggestion.StateLogEntry.Index && s.Term == suggestion.StateLogEntry.Term) || s.Term > suggestion.StateLogEntry.Term); col.Insert(suggestion.StateLogEntry); //using (var t = this.db.GetTransaction()) { //foreach (var el in t.SelectForwardFromTo<byte[], byte[]>(stateTableName, // new byte[] { 1 }.ToBytes(suggestion.StateLogEntry.Index, ulong.MinValue), true, // new byte[] { 1 }.ToBytes(ulong.MaxValue, ulong.MaxValue), true, true)) //{ // if ( // el.Key.Substring(1, 8).To_UInt64_BigEndian() == suggestion.StateLogEntry.Index && // el.Key.Substring(9, 8).To_UInt64_BigEndian() == suggestion.StateLogEntry.Term // ) // { // return; // } // t.RemoveKey<byte[]>(stateTableName, el.Key); // // pups++; //} //t.Insert<byte[], byte[]>(stateTableName, new byte[] { 1 }.ToBytes(suggestion.StateLogEntry.Index, suggestion.StateLogEntry.Term), suggestion.StateLogEntry.SerializeBiser()); //t.Commit(); } statemachine.VerbosePrint($"{statemachine.NodeAddress.NodeAddressId}> AddToLogFollower (I/T): {suggestion.StateLogEntry.Index}/{suggestion.StateLogEntry.Term} -> Result:" + $" { (GetEntryByIndexTerm(suggestion.StateLogEntry.Index, suggestion.StateLogEntry.Term) != null)};"); //Setting new internal LogId PreviousStateLogId = StateLogId; PreviousStateLogTerm = StateLogTerm; StateLogId = suggestion.StateLogEntry.Index; StateLogTerm = suggestion.StateLogEntry.Term; tempPrevStateLogId = PreviousStateLogId; tempPrevStateLogTerm = PreviousStateLogTerm; tempStateLogId = StateLogId; tempStateLogTerm = StateLogTerm; if (suggestion.IsCommitted) { if ( this.LastCommittedIndexTerm > suggestion.StateLogEntry.Term || ( this.LastCommittedIndexTerm == suggestion.StateLogEntry.Term && this.LastCommittedIndex > suggestion.StateLogEntry.Index ) ) { //Should be not possible } else { this.LastCommittedIndex = suggestion.StateLogEntry.Index; this.LastCommittedIndexTerm = suggestion.StateLogEntry.Term; this.statemachine.logHandler.Commited(); } } } catch (Exception ex) { } if (statemachine.States.LeaderHeartbeat != null && this.LastCommittedIndex < statemachine.States.LeaderHeartbeat.LastStateLogCommittedIndex) { statemachine.SyncronizeWithLeader(true); } else { LeaderSynchronizationIsActive = false; } }
/// <summary> /// + /// Can be null. /// Must be called inside of operation lock. /// get local commit history for follower /// </summary> /// <param name="logEntryId"></param> /// <param name="LeaderTerm"></param> /// <returns></returns> public StateLogEntrySuggestion GetNextStateLogEntrySuggestionFromRequested(StateLogEntryRequest req) { StateLogEntrySuggestion le = new StateLogEntrySuggestion() { LeaderTerm = statemachine.NodeTerm }; int cnt = 0; StateLogEntry sle = null; ulong prevId = 0; ulong prevTerm = 0; var col = this.db.GetCollection <StateLogEntry>(stateTableName); //using (var t = this.db.GetTransaction()) { if (req.StateLogEntryId == 0) { var trow = col.Query().OrderBy(s => s.Term, 1).OrderBy(s => s.Term, 1).FirstOrDefault(); if (trow != null) { le.StateLogEntry = trow; if (LastCommittedIndexTerm >= le.StateLogEntry.Term && LastCommittedIndex >= le.StateLogEntry.Index ) { le.IsCommitted = true; } cnt = 2; } } else { var list = col.Query().Where(s => s.Index >= req.StateLogEntryId).ToList().OrderBy(s => s.Term).OrderBy(s => s.Term).ToList(); foreach (var item in list) { cnt++; sle = item; if (cnt == 1) { prevId = sle.Index; prevTerm = sle.Term; } else { le.StateLogEntry = sle; } } if (cnt < 2) { ulong toAdd = (ulong)cnt; list = col.Query().Where(s => s.Index >= req.StateLogEntryId + toAdd).ToList().OrderBy(s => s.Term).OrderBy(s => s.Index).ToList(); foreach (var item in list) { cnt++; sle = item; if (cnt == 1) { prevId = sle.Index; prevTerm = sle.Term; } else { le.StateLogEntry = sle; } } } } //if (req.StateLogEntryId == 0)// && req.StateLogEntryTerm == 0) //{ // var trow = t.SelectForwardFromTo<byte[], byte[]>(stateTableName, // new byte[] { 1 }.ToBytes(ulong.MinValue, ulong.MinValue), true, // new byte[] { 1 }.ToBytes(ulong.MaxValue, ulong.MaxValue), true).FirstOrDefault(); // if (trow != null && trow.Exists) // sle = StateLogEntry.BiserDecode(trow.Value); // if (sle != null) // { // le.StateLogEntry = sle; // if ( // LastCommittedIndexTerm >= le.StateLogEntry.Term // && // LastCommittedIndex >= le.StateLogEntry.Index // ) // { // le.IsCommitted = true; // } // cnt = 2; // } //} //else //{ // Tuple<ulong, StateLogEntry> sleTpl; // bool reForward = true; // reForward = false; // foreach (var el in t.SelectForwardFromTo<byte[], byte[]>(stateTableName, // new byte[] { 1 }.ToBytes(req.StateLogEntryId, ulong.MinValue), true, // new byte[] { 1 }.ToBytes(ulong.MaxValue, ulong.MaxValue), true).Take(2)) // { // cnt++; // sle = StateLogEntry.BiserDecode(el.Value); // if (cnt == 1) // { // prevId = sle.Index; // prevTerm = sle.Term; // } // else // { // le.StateLogEntry = sle; // } // } // if (cnt < 2 && reForward) // { // ulong toAdd = (ulong)cnt; // foreach (var el in t.SelectForwardFromTo<byte[], byte[]>(stateTableName, // //new byte[] { 1 }.ToBytes(req.StateLogEntryId + toAdd, req.StateLogEntryTerm), true, // new byte[] { 1 }.ToBytes(req.StateLogEntryId + toAdd, ulong.MinValue), true, // new byte[] { 1 }.ToBytes(ulong.MaxValue, ulong.MaxValue), true).Take(2)) // { // cnt++; // sle = StateLogEntry.BiserDecode(el.Value); // if (cnt == 1) // { // prevId = sle.Index; // prevTerm = sle.Term; // } // else // { // le.StateLogEntry = sle; // } // } } if (cnt == 2) { le.StateLogEntry.PreviousStateLogId = prevId; le.StateLogEntry.PreviousStateLogTerm = prevTerm; if ( LastCommittedIndexTerm >= le.StateLogEntry.Term && LastCommittedIndex >= le.StateLogEntry.Index ) { le.IsCommitted = true; } } if (cnt != 2) { return(null); } return(le); }
/// <summary> /// /// </summary> /// <param name="suggestion"></param> /// <returns></returns> public void AddLogEntryByFollower(StateLogEntrySuggestion suggestion) { try { using (var t = this.db.GetTransaction()) { foreach (var el in t.SelectForwardFromTo <byte[], byte[]>(stateTableName, new byte[] { 1 }.ToBytes(suggestion.StateLogEntry.Index, ulong.MinValue), true, new byte[] { 1 }.ToBytes(ulong.MaxValue, ulong.MaxValue), true, true)) { if ( el.Key.Substring(1, 8).To_UInt64_BigEndian() == suggestion.StateLogEntry.Index && el.Key.Substring(9, 8).To_UInt64_BigEndian() == suggestion.StateLogEntry.Term ) { return; } t.RemoveKey <byte[]>(stateTableName, el.Key); // pups++; } t.Insert <byte[], byte[]>(stateTableName, new byte[] { 1 }.ToBytes(suggestion.StateLogEntry.Index, suggestion.StateLogEntry.Term), suggestion.StateLogEntry.SerializeBiser()); t.Commit(); } // statemachine.VerbosePrint($"{statemachine.NodeAddress.NodeAddressId}> AddToLogFollower (I/T): {suggestion.StateLogEntry.Index}/{suggestion.StateLogEntry.Term} -> Result:" + // $" { (GetEntryByIndexTerm(suggestion.StateLogEntry.Index, suggestion.StateLogEntry.Term) != null)};"); //Setting new internal LogId PreviousStateLogId = StateLogId; PreviousStateLogTerm = StateLogTerm; StateLogId = suggestion.StateLogEntry.Index; StateLogTerm = suggestion.StateLogEntry.Term; //tempPrevStateLogId = PreviousStateLogId; //tempPrevStateLogTerm = PreviousStateLogTerm; //tempStateLogId = StateLogId; //tempStateLogTerm = StateLogTerm; if (suggestion.IsCommitted) { if ( this.LastCommittedIndexTerm > suggestion.StateLogEntry.Term || ( this.LastCommittedIndexTerm == suggestion.StateLogEntry.Term && this.LastCommittedIndex > suggestion.StateLogEntry.Index ) ) { //Should be not possible } else { this.LastCommittedIndex = suggestion.StateLogEntry.Index; this.LastCommittedIndexTerm = suggestion.StateLogEntry.Term; } } } catch (Exception ex) { } }
/// <summary> /// + /// Can be null. /// Must be called inside of operation lock. /// get local commit history for follower /// </summary> /// <param name="logEntryId"></param> /// <param name="LeaderTerm"></param> /// <returns></returns> public StateLogEntrySuggestion GetNextStateLogEntrySuggestion(StateLogEntryRequest req) { StateLogEntrySuggestion le = new StateLogEntrySuggestion() { LeaderTerm = statemachine.NodeTerm }; int cnt = 0; StateLogEntry sle = null; ulong prevId = 0; ulong prevTerm = 0; using (var t = this.db.GetTransaction()) { if (req.StateLogEntryId == 0)// && req.StateLogEntryTerm == 0) { var trow = t.SelectForwardFromTo <byte[], byte[]>(stateTableName, new byte[] { 1 }.ToBytes(ulong.MinValue, ulong.MinValue), true, new byte[] { 1 }.ToBytes(ulong.MaxValue, ulong.MaxValue), true).FirstOrDefault(); if (trow != null && trow.Exists) { sle = StateLogEntry.BiserDecode(trow.Value); } if (sle != null) { le.StateLogEntry = sle; if ( LastCommittedIndexTerm >= le.StateLogEntry.Term && LastCommittedIndex >= le.StateLogEntry.Index ) { le.IsCommitted = true; } cnt = 2; } } else { Tuple <ulong, StateLogEntry> sleTpl; bool reForward = true; reForward = false; foreach (var el in t.SelectForwardFromTo <byte[], byte[]>(stateTableName, new byte[] { 1 }.ToBytes(req.StateLogEntryId, ulong.MinValue), true, new byte[] { 1 }.ToBytes(ulong.MaxValue, ulong.MaxValue), true).Take(2)) { cnt++; sle = StateLogEntry.BiserDecode(el.Value); if (cnt == 1) { prevId = sle.Index; prevTerm = sle.Term; } else { le.StateLogEntry = sle; } } if (cnt < 2 && reForward) { ulong toAdd = (ulong)cnt; foreach (var el in t.SelectForwardFromTo <byte[], byte[]>(stateTableName, //new byte[] { 1 }.ToBytes(req.StateLogEntryId + toAdd, req.StateLogEntryTerm), true, new byte[] { 1 }.ToBytes(req.StateLogEntryId + toAdd, ulong.MinValue), true, new byte[] { 1 }.ToBytes(ulong.MaxValue, ulong.MaxValue), true).Take(2)) { cnt++; sle = StateLogEntry.BiserDecode(el.Value); if (cnt == 1) { prevId = sle.Index; prevTerm = sle.Term; } else { le.StateLogEntry = sle; } } } if (cnt == 2) { le.StateLogEntry.PreviousStateLogId = prevId; le.StateLogEntry.PreviousStateLogTerm = prevTerm; if ( LastCommittedIndexTerm >= le.StateLogEntry.Term && LastCommittedIndex >= le.StateLogEntry.Index ) { le.IsCommitted = true; } } } } if (cnt != 2) { return(null); } return(le); }