/// <summary> /// Updates a message file's metadata. /// </summary> /// <param name="path">The file path.</param> /// <param name="msgInfo">The message metadata.</param> internal void WriteMessageMetadata(string path, QueuedMsgInfo msgInfo) { using (var fsMsg = new EnhancedFileStream(path, FileMode.Open, FileAccess.ReadWrite)) { // Seek past the message's header and body. int cbBody; byte[] md5Hash; fsMsg.Position = MsgHeaderSize; cbBody = fsMsg.ReadInt32(); fsMsg.Position = fsMsg.Position + cbBody; // Write the metadata and truncate the file. fsMsg.WriteString16(msgInfo.ToString()); fsMsg.SetLength(fsMsg.Position); // Regenerate the MD5 Hash fsMsg.Position = MsgMD5Offset + MD5Hasher.DigestSize; md5Hash = MD5Hasher.Compute(fsMsg, fsMsg.Length - fsMsg.Position); fsMsg.Position = MsgMD5Offset; fsMsg.WriteBytesNoLen(md5Hash); } }
/// <summary> /// Truncates the log to the position passed. /// </summary> /// <param name="position">The <see cref="ILogPosition" /> defining where the truncation should occur.</param> /// <remarks> /// <note> /// This property can only be called if the operation log is in <see cref="OperationLogMode.Undo" /> /// mode. /// </note> /// <para> /// This method is used in combination with the <see cref="Position" /> property to roll /// back operations within a base transaction. /// </para> /// </remarks> /// <exception cref="TransactionException">Thrown if the log isn't open or if the mode isn't <see cref="OperationLogMode.Undo" />.</exception> public void Truncate(ILogPosition position) { using (TimedLock.Lock(this)) { if (file == null) { throw new TransactionException(ClosedMsg); } if (mode != OperationLogMode.Undo) { throw new TransactionException("Write is available only when the log is in UNDO mode."); } file.SetLength(((FileLogPosition)position).Position); } }
/// <summary> /// Loads the message index file. If the index does not exist or is corrupt then /// the index will be rebuilt by performing a full scan of message files under /// the root folder. /// </summary> /// <param name="newIndexFile">Pass as <c>true</c> if a new index file is being created.</param> private void LoadIndex(bool newIndexFile) { string formatErr = string.Format("Invalid or missing index file [{0}].", indexPath); int cMessages; string[] files; Dictionary <Guid, string> msgFiles; TimedLock.AssertLocked(this); messages = new Dictionary <Guid, QueuedMsgInfo>(); // List the message files and build a hash table mapping each message GUID // the fully qualified path to the file. files = Helper.GetFilesByPattern(root + "*.msg", SearchOption.AllDirectories); msgFiles = new Dictionary <Guid, string>(files.Length); foreach (string path in files) { string file = Path.GetFileName(path).ToLowerInvariant(); Guid msgID; if (!file.EndsWith(".msg")) { continue; } try { msgID = new Guid(file.Substring(0, file.Length - 4)); } catch { continue; } msgFiles[msgID] = path; } // Load the index file. try { // Parse the index file header fsIndex.Position = 0; if (fsIndex.ReadInt32() != IndexMagic) // Magic Number { throw new FormatException(formatErr); } if (fsIndex.ReadInt32() != 0) // Format Version { throw new FormatException(formatErr); } fsIndex.ReadInt32(); // Reserved if (fsIndex.ReadInt32() != 0) { throw new FormatException(string.Format("Index file [{0}] was not closed properly. Full message folder scan will be performed.", indexPath)); } cMessages = fsIndex.ReadInt32(); // Message Count // Parse the message metadata for (int i = 0; i < cMessages; i++) { QueuedMsgInfo msgInfo; msgInfo = new QueuedMsgInfo(fsIndex.ReadString16()); msgInfo.PersistID = msgInfo.ID; msgInfo.ProviderData = root + fsIndex.ReadString16(); // Make the paths absolute messages[msgInfo.ID] = msgInfo; } // Perform an extra consistency check by listing all of the message files // under the root folder and comparing the message GUIDs encoded into the // file names with the GUIDs loaded from the index file and then bringing // the index into sync with the actual message files. bool updated = false; int cLoaded = 0; // Delete any metadata for messages that exist in the index // but don't exist on the file system. var delList = new List <Guid>(); foreach (Guid msgID in messages.Keys) { if (!msgFiles.ContainsKey(msgID)) { delList.Add(msgID); } } foreach (Guid msgID in delList) { messages.Remove(msgID); } if (delList.Count > 0) { updated = true; SysLog.LogWarning(string.Format("Message index [{0}] has message metadata for messages that do not exist. [{1}] messages will be removed from the index.", indexPath, delList.Count)); } // Load metadata for messages that exist in the file system // but were not in the index. foreach (Guid msgID in msgFiles.Keys) { if (!messages.ContainsKey(msgID)) { string path = msgFiles[msgID]; try { messages[msgID] = ReadMessageMetadata(msgID, path); cLoaded++; } catch (Exception e) { SysLog.LogException(e); } } } if (cLoaded > 0) { updated = true; SysLog.LogWarning(string.Format("Message index [{0}] is missing metadata for [{1}] messages. Missing entries will be added.", indexPath, cLoaded)); } if (updated) { SaveIndex(true); } // Mark the index as "open" for crash detection. fsIndex.Position = OpenFlagOffset; fsIndex.WriteInt32(1); fsIndex.Flush(); } catch { if (newIndexFile) { SysLog.LogWarning("Rebuilding missing message index file [{0}].", indexPath); } else { SysLog.LogWarning("Rebuilding corrupt message index file [{0}].", indexPath); } // Clear the index file if there was a serious error and then // rebuild it from scratch by scanning the message metadata. fsIndex.SetLength(0); fsIndex.Flush(); messages.Clear(); foreach (Guid msgID in msgFiles.Keys) { try { messages.Add(msgID, ReadMessageMetadata(msgID, msgFiles[msgID])); } catch (Exception e2) { SysLog.LogException(e2); } } // Save the index, marking the file as "open" for crash detection SaveIndex(true); } }
public void MsgQueueFileStore_Corrupt_Messages() { MsgQueueFileStore store = null; int count; QueuedMsgInfo msgInfo; QueuedMsg msg; object persistID; Guid[] ids; Dictionary <Guid, bool> corruptIDs; List <string> corruptFiles; // Create a store with a bunch of messages then close // it and corrupt some of the message files. Then attempt // to open the files and verify that the corrupted files // are detected. ClearFolders(); try { store = new MsgQueueFileStore(root); store.Open(); ids = new Guid[MessageCount]; for (int i = 0; i < MessageCount; i++) { msg = new QueuedMsg(); msg.TargetEP = "logical://test/" + i.ToString(); msg.Body = i; msgInfo = new QueuedMsgInfo(null, msg); store.Add(msgInfo, msg); ids[i] = msg.ID; } Assert.AreEqual(MessageCount, store.Count); count = 0; foreach (QueuedMsgInfo i in store) { count++; } Assert.AreEqual(MessageCount, count); for (int i = 0; i < ids.Length; i++) { Guid id = ids[i]; persistID = store.GetPersistID(id); Assert.IsNotNull(persistID); msgInfo = store.GetInfo(persistID); Assert.IsNotNull(msgInfo); Assert.AreEqual((MsgEP)("logical://test/" + i.ToString()), msgInfo.TargetEP); msg = store.Get(persistID); msg.DeserializedBody(); Assert.AreEqual(i, (int)msg.Body); } // Corrupt 1/2 of the message files in various ways. corruptIDs = new Dictionary <Guid, bool>(); corruptFiles = new List <string>(); for (int i = 0; i < MessageCount / 2; i++) { corruptIDs.Add(ids[i], true); corruptFiles.Add(store.GetMessagePath(ids[i], false)); } int cbTruncate = 0; for (int i = 0; i < corruptFiles.Count; i++) { string file = corruptFiles[i]; using (EnhancedFileStream fs = new EnhancedFileStream(file, FileMode.Open, FileAccess.ReadWrite)) { if ((i & 1) == 0) { // Truncate the file by at least one byte int cb; cb = Math.Min((int)fs.Length - 1, cbTruncate++); fs.SetLength(cb); } else { // Mess with a byte at random position in the file. int pos = Helper.Rand((int)fs.Length); byte b; fs.Position = pos; b = (byte)fs.ReadByte(); fs.Position = pos; fs.WriteByte((byte)(~b)); } } } // Load all of the message files and verify that the corrupt files // are detected. for (int i = 0; i < ids.Length; i++) { Guid id = ids[i]; if (corruptIDs.ContainsKey(id)) { try { persistID = store.GetPersistID(id); Assert.IsNotNull(persistID); msgInfo = store.GetInfo(persistID); Assert.IsNotNull(msgInfo); Assert.AreEqual((MsgEP)("logical://test/" + i.ToString()), msgInfo.TargetEP); msg = store.Get(persistID); Assert.Fail("Exception expected"); } catch { // Expecting an exception } } else { persistID = store.GetPersistID(id); Assert.IsNotNull(persistID); msgInfo = store.GetInfo(persistID); Assert.IsNotNull(msgInfo); Assert.AreEqual((MsgEP)("logical://test/" + i.ToString()), msgInfo.TargetEP); msg = store.Get(persistID); msg.DeserializedBody(); Assert.AreEqual(i, (int)msg.Body); } } } finally { if (store != null) { store.Close(); } } }