I am developing an application that works with files and folder on a network share (similar to the windows explorer). The application is used by multiple users and provides some commands for modifying the file system.
Problem
The parallel access to the file system may result in conflicts.
Example:
User A renames a folder (FolderA) while user B is executing a long-running process that moves / creates / deletes some of the sub folders of folder B. Therefore the rename breaks the operation of user B.
Requirements
- An application should be able to lock folders if required.
- Other applications should be notified as soon as possible about locks, so that these applications may disable corresponding commands (e.g. Rename will be disabled for a folder where any child folder is locked).
- All kinds of network shares should be supported (windwos, samba, ...)
- The application should be able to show the locking user / and timestamp.
Approach
To avoid conflicts as described above, a centralized locking mechanism should be introduced.
The idea is, to create a file on the network share. That file contains all current locks (one per line). Format: "RelativePath|User|DateTime".
Each application checks in certain intervals (e.g. 100 ms) if the file changed and updates it's internal state accordingly.
Each application may add / remove entries to / from the file.
Implementation
The API to the storage uses the following simple interface:
ILockStorage.cs
public interface ILockStorage : IDisposable
{
event EventHandler<LockEntriesChangedEventArgs> LockEntriesChanged;
void AddEntry(LockEntry entryToAdd);
void RemoveEntry(LockEntry entryToRemove);
}
LockEntriesChangedEventArgs.cs
public class LockEntriesChangedEventArgs : EventArgs
{
private readonly LockEntry[] myEntriesAdded;
private readonly LockEntry[] myEntriesRemoved;
public LockEntriesChangedEventArgs(LockEntry[] entriesAdded, LockEntry[] EntriesRemoved)
{
myEntriesAdded = entriesAdded;
myEntriesRemoved = EntriesRemoved;
}
public LockEntry[] EntriesAdded { get { return myEntriesAdded; } }
public LockEntry[] EntriesRemoved { get { return myEntriesRemoved; } }
}
LockEntry.cs
public class LockEntry : IEquatable<LockEntry>
{
private readonly string myPath;
private readonly string myUser;
private readonly DateTime myLockTime;
public LockEntry(string path, string user, DateTime lockTime)
{
path.Ensure("path").IsNotNull();
user.Ensure("user").IsNotNull();
myPath = path.ToLower().Trim('\\');
myUser = user;
myLockTime = lockTime;
}
public string User { get { return myUser; } }
public string Path { get { return myPath; } }
public DateTime LockTime { get { return myLockTime; } }
public static bool TryParse(string input, out LockEntry lockEntry)
{
lockEntry = null;
if (string.IsNullOrWhiteSpace(input))
return false;
var entryParts = input.Split('|');
if (entryParts.Length != 3)
return false;
var path = entryParts[0];
var user = entryParts[1];
var lockTimeString = entryParts[2];
DateTime lockTime;
if (string.IsNullOrWhiteSpace(path) ||
string.IsNullOrWhiteSpace(user) ||
!DateTime.TryParse(lockTimeString, CultureInfo.InvariantCulture, DateTimeStyles.None, out lockTime))
return false;
lockEntry = new LockEntry(path, user, lockTime);
return true;
}
public override string ToString()
{
return string.Format("{0}|{1}|{2}", Path, User, LockTime.ToString(CultureInfo.InvariantCulture));
}
public override bool Equals(object obj)
{
var other = obj as LockEntry;
if (other == null)
return false;
return ((IEquatable<LockEntry>)this).Equals(other);
}
public override int GetHashCode()
{
return myLockTime.GetHashCode() ^
myUser.GetHashCode() ^
myPath.GetHashCode();
}
bool IEquatable<LockEntry>.Equals(LockEntry other)
{
return myLockTime == other.LockTime &&
myUser == other.User &&
myPath.Equals(other.Path);
}
}
The implementation of the interface:
LockStorage.cs
internal class LockStorage : ILockStorage
{
private const int WRITE_RETRY_COUNT = 20;
private const int WRITE_RETRY_INTERVAL_MS = 50;
private readonly Timer myTimer;
private readonly FileInfo myFileInfo;
private LockEntry[] myLockEntries = new LockEntry[0];
private DateTime myLastSyncTimestamp = DateTime.MinValue;
private bool myIsDisposed;
public event EventHandler<LockEntriesChangedEventArgs> LockEntriesChanged;
/* Nested Types
* ********************************************************************/
private abstract class LockEntryWriter
{
private readonly LockStorage myLockStorage;
protected LockEntryWriter(LockStorage lockStorage)
{
myLockStorage = lockStorage;
}
public void Write()
{
myLockStorage.myTimer.Stop();
try
{
IOException lastException = null;
for (int i = 0; i < WRITE_RETRY_COUNT; i++)
{
try
{
WriteInternal();
return;
}
catch (IOException ex)
{
// may occur if the file stream is open for writing / readings
lastException = ex;
Thread.Sleep(WRITE_RETRY_INTERVAL_MS);
}
}
// write failed
throw new IOException("Unable to write to lock file. (retried " + WRITE_RETRY_COUNT + " times)", lastException);
}
finally
{
myLockStorage.myTimer.Start();
}
}
private void WriteInternal()
{
LockEntry[] entries;
using (var writeStream = OpenWriteStream(out entries))
{
if (!IsWriteRequired(entries))
return;
using (var writer = new StreamWriter(writeStream))
{
Write(writer, entries);
// truncate file
writeStream.SetLength(writeStream.Position);
}
}
}
private Stream OpenWriteStream(out LockEntry[] entries)
{
var fileInfo = myLockStorage.myFileInfo;
var lastLyncTimestamp = myLockStorage.myLastSyncTimestamp;
var readWriteStream = fileInfo.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
fileInfo.Refresh();
entries = myLockStorage.LockEntries;
if (lastLyncTimestamp < fileInfo.LastWriteTime)
{
var lines = ReadLockFile(readWriteStream);
entries = ToLockEntries(lines.ToArray());
}
readWriteStream.Position = 0;
return readWriteStream;
}
protected abstract bool IsWriteRequired(LockEntry[] currentEntries);
protected abstract void Write(StreamWriter writer, LockEntry[] currentEntries);
}
private class AddLockEntryWriter : LockEntryWriter
{
private readonly LockEntry myEntryToAdd;
public AddLockEntryWriter(LockStorage lockStorage, LockEntry entryToAdd)
: base(lockStorage)
{
myEntryToAdd = entryToAdd;
}
protected override bool IsWriteRequired(LockEntry[] currentEntries)
{
return !currentEntries.Contains(myEntryToAdd);
}
protected override void Write(StreamWriter writer, LockEntry[] currentEntries)
{
foreach (var entry in currentEntries)
writer.WriteLine(entry.ToString());
writer.WriteLine(myEntryToAdd.ToString());
}
}
private class RemoveLockEntryWriter : LockEntryWriter
{
private readonly LockEntry myEntryToRemove;
public RemoveLockEntryWriter(LockStorage lockStorage, LockEntry entryToRemove)
: base(lockStorage)
{
myEntryToRemove = entryToRemove;
}
protected override bool IsWriteRequired(LockEntry[] currentEntries)
{
return currentEntries.Contains(myEntryToRemove);
}
protected override void Write(StreamWriter writer, LockEntry[] currentEntries)
{
foreach (var entry in currentEntries.Except(new[] { myEntryToRemove }))
writer.WriteLine(entry.ToString());
}
}
/* .ctor
* ********************************************************************/
internal LockStorage(string lockfilePath, TimeSpan intervall)
{
myFileInfo = new FileInfo(lockfilePath);
UpdateInternalState();
myTimer = new Timer(intervall.Milliseconds);
myTimer.AutoReset = false;
myTimer.Elapsed += TimerElapsed;
myTimer.Start();
}
/* public API
* ********************************************************************/
public LockEntry[] LockEntries { get { return myLockEntries.ToArray(); } }
public void AddEntry(LockEntry entryToAdd)
{
new AddLockEntryWriter(this, entryToAdd).Write();
}
public void RemoveEntry(LockEntry entryToRemove)
{
new RemoveLockEntryWriter(this, entryToRemove).Write();
}
/* privates
* ********************************************************************/
private void TimerElapsed(object sender, ElapsedEventArgs e)
{
try
{
myFileInfo.Refresh();
if (myFileInfo.LastWriteTime == myLastSyncTimestamp)
return;
UpdateInternalState();
}
catch (Exception ex)
{
// LogError("Error while reloading lock file", ex);
}
finally
{
myTimer.Start();
}
}
private void UpdateInternalState()
{
try
{
string[] lines;
using (var stream = myFileInfo.Open(FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read))
lines = ReadLockFile(stream);
if (lines == null)
return;
var lockEntries = ToLockEntries(lines);
myLastSyncTimestamp = myFileInfo.LastWriteTime;
UpdateLockEntries(lockEntries);
}
catch (IOException)
{
/* Ignore: May be occur if the file is being written at the moment */
}
}
private void UpdateLockEntries(LockEntry[] lockEntries)
{
var entriesToRemove = new List<LockEntry>(myLockEntries);
var addedEntries = new List<LockEntry>();
foreach (var lockEntry in lockEntries)
{
entriesToRemove.Remove(lockEntry);
if (myLockEntries.Contains(lockEntry))
continue;
addedEntries.Add(lockEntry);
}
if (addedEntries.Any() || entriesToRemove.Any())
{
myLockEntries = myLockEntries.Except(entriesToRemove)
.Concat(addedEntries)
.ToArray();
OnLockEntriesChanged(addedEntries.ToArray(), entriesToRemove.ToArray());
}
}
private static LockEntry[] ToLockEntries(string[] lines)
{
var lockEntries = new List<LockEntry>();
foreach (var line in lines)
{
LockEntry lockEntry;
if (!LockEntry.TryParse(line, out lockEntry))
{
//LogWarning("Unable to parse lock file entry'" + line + "'. Entry skipped.");
continue;
}
lockEntries.Add(lockEntry);
}
return lockEntries.ToArray();
}
private static string[] ReadLockFile(Stream stream)
{
var lines = new List<string>();
using (var reader = new StreamReader(stream, Encoding.UTF8, true, 1024, true))
while (!reader.EndOfStream)
lines.Add(reader.ReadLine());
return lines.ToArray();
}
private void OnLockEntriesChanged(LockEntry[] entriesAdded, LockEntry[] entriesRemoved)
{
var handler = LockEntriesChanged;
if (handler != null)
handler(this, new LockEntriesChangedEventArgs(entriesAdded, entriesRemoved));
}
/* IDisposable implementation
* ********************************************************************/
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (myIsDisposed)
return;
if (disposing)
{
myTimer.Stop();
myTimer.Dispose();
}
myIsDisposed = true;
}
}
I am primary interested in comments related to stability and robustness.
- Are there any use cases that would break the storage?
- Any alternative solutions for the problem of concurrent file access (read/write)?
However, all kinds of comments are welcome of course.