C#: a write through a live link at the store path is refused; read-under-lock pinned (store-link-cs) (#151)
This commit is contained in:
parent
3a8de542de
commit
73e2131d9a
8 changed files with 674 additions and 32 deletions
|
|
@ -710,17 +710,23 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
// Only a writer creates the records folder and the lock, so opening the store creates nothing. A reader opens an
|
||||
// existing lock and returns null when there is none; that does not mean no writer committed (a store may be copied
|
||||
// without its lock, and a first writer may create it mid-load), so an unleased load checks that it settled.
|
||||
// A writer's lease is also where a link at the manifest path is refused (a commit would replace the link with a file): before
|
||||
// the records folder or the lock is made, and again once the lease is held, because a link can be put there while it waited.
|
||||
private FileStream? AcquireCommitLease(bool wait, bool create)
|
||||
{
|
||||
if (create)
|
||||
{
|
||||
CultStoreLinks.Refuse(_manifestFile.FullName);
|
||||
Directory.CreateDirectory(_recordDirectory.FullName);
|
||||
}
|
||||
var lockPath = Path.Combine(_recordDirectory.FullName, ".commit.lock");
|
||||
var started = Stopwatch.StartNew();
|
||||
while (true)
|
||||
{
|
||||
FileStream held;
|
||||
try
|
||||
{
|
||||
return new FileStream(
|
||||
held = new FileStream(
|
||||
lockPath,
|
||||
create ? FileMode.OpenOrCreate : FileMode.Open,
|
||||
FileAccess.ReadWrite,
|
||||
|
|
@ -743,7 +749,23 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
catch (IOException) when (started.Elapsed < TimeSpan.FromSeconds(30))
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (create)
|
||||
{
|
||||
try
|
||||
{
|
||||
CultStoreLinks.Refuse(_manifestFile.FullName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
held.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return held;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5132,7 +5132,7 @@ namespace GameCult.Caching
|
|||
var attributes = FileInfo.Attributes;
|
||||
if ((int)attributes == -1)
|
||||
return null;
|
||||
if ((attributes & FileAttributes.Directory) != 0 && (attributes & FileAttributes.ReparsePoint) == 0)
|
||||
if ((attributes & FileAttributes.Directory) != 0 && Directory.Exists(FileInfo.FullName))
|
||||
throw new IOException("The store path is a directory, not a store file.");
|
||||
try
|
||||
{
|
||||
|
|
@ -5154,25 +5154,27 @@ namespace GameCult.Caching
|
|||
}
|
||||
|
||||
// Something is at the path even when the path cannot be opened: a link keeps the reparse-point attribute whether or not its
|
||||
// target is there. A path with nothing at it reports no attributes at all (-1).
|
||||
private bool PathIsALink()
|
||||
{
|
||||
FileInfo.Refresh();
|
||||
var attributes = FileInfo.Attributes;
|
||||
return (int)attributes != -1 && (attributes & FileAttributes.ReparsePoint) != 0;
|
||||
}
|
||||
// target is there.
|
||||
private bool PathIsALink() => CultStoreLinks.IsLink(FileInfo.FullName);
|
||||
|
||||
// A write never goes through a link at the store path: replacing it would silently detach the store from its link.
|
||||
private void RefuseLinkedStore() => CultStoreLinks.Refuse(FileInfo.FullName);
|
||||
|
||||
// The lock is a sidecar opened exclusively, which excludes other handles in this process and in others alike.
|
||||
// Every write takes the lock first, so the link is refused here twice: before the lock file or anything else is made, and
|
||||
// again once the lock is held, because a link can be put at the path while this writer waited on another's lock.
|
||||
private FileStream? AcquireLock(bool wait)
|
||||
{
|
||||
RefuseLinkedStore();
|
||||
Directory.CreateDirectory(FileInfo.DirectoryName!);
|
||||
var lockPath = FileInfo.FullName + ".lock";
|
||||
var started = Stopwatch.StartNew();
|
||||
while (true)
|
||||
{
|
||||
FileStream held;
|
||||
try
|
||||
{
|
||||
return new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, bufferSize: 1);
|
||||
held = new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, bufferSize: 1);
|
||||
}
|
||||
catch (IOException) when (!wait)
|
||||
{
|
||||
|
|
@ -5181,7 +5183,20 @@ namespace GameCult.Caching
|
|||
catch (IOException) when (started.Elapsed < TimeSpan.FromSeconds(30))
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RefuseLinkedStore();
|
||||
}
|
||||
catch
|
||||
{
|
||||
held.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
return held;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
24
src/GameCult.Caching/CultStoreLinkedException.cs
Normal file
24
src/GameCult.Caching/CultStoreLinkedException.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using System.IO;
|
||||
|
||||
namespace GameCult.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// A write was attempted at a store path that is a link (a symbolic link to a file or a directory, a Windows junction or any
|
||||
/// other reparse-point link). The write is refused before the lock, the staging file or the replace, and again once the lock
|
||||
/// is held, so a link that exists when a writer takes the lock leaves the link and its target untouched. A link created by
|
||||
/// something that ignores the lock after that check, while the commit builds and writes the new image, is still replaced;
|
||||
/// that window cannot be closed without native no-follow opens. Configure the link's target as the store path to write to
|
||||
/// it; reads through a live link are unchanged.
|
||||
/// </summary>
|
||||
public sealed class CultStoreLinkedException : IOException
|
||||
{
|
||||
public CultStoreLinkedException(string path)
|
||||
: base($"CultCache store {path} is a link, and a write at it would replace the link; configure the link's target as the store path.")
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
|
||||
/// <summary>The store path that is a link.</summary>
|
||||
public string Path { get; }
|
||||
}
|
||||
}
|
||||
22
src/GameCult.Caching/CultStoreLinks.cs
Normal file
22
src/GameCult.Caching/CultStoreLinks.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using System.IO;
|
||||
|
||||
namespace GameCult.Caching
|
||||
{
|
||||
// The one decision on whether a store-owned file is a link, shared by every store kind that replaces files.
|
||||
internal static class CultStoreLinks
|
||||
{
|
||||
// A path with nothing at it reports no attributes at all (-1). Any reparse point counts as a link, whether or not its
|
||||
// target is there.
|
||||
internal static bool IsLink(string path)
|
||||
{
|
||||
var attributes = new FileInfo(path).Attributes;
|
||||
return (int)attributes != -1 && (attributes & FileAttributes.ReparsePoint) != 0;
|
||||
}
|
||||
|
||||
internal static void Refuse(string path)
|
||||
{
|
||||
if (IsLink(path))
|
||||
throw new CultStoreLinkedException(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue