-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update position before ReadAsync starts, but fix it after incomplete read #56531
Changes from all commits
be0b957
bc86bb2
55c043d
079ed8b
30b5768
c513ce5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,40 +27,29 @@ public override Task<int> ReadAsync(byte[] buffer, int offset, int count, Cancel | |
public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default) | ||
=> ReadAsyncInternal(destination, cancellationToken); | ||
|
||
private unsafe ValueTask<int> ReadAsyncInternal(Memory<byte> destination, CancellationToken cancellationToken) | ||
private ValueTask<int> ReadAsyncInternal(Memory<byte> destination, CancellationToken cancellationToken) | ||
{ | ||
if (!CanRead) | ||
if (!CanSeek) | ||
{ | ||
ThrowHelper.ThrowNotSupportedException_UnreadableStream(); | ||
return RandomAccess.ReadAtOffsetAsync(_fileHandle, destination, fileOffset: -1, cancellationToken); | ||
} | ||
|
||
long positionBefore = _filePosition; | ||
if (CanSeek) | ||
if (LengthCachingSupported && _length >= 0 && Volatile.Read(ref _filePosition) >= _length) | ||
{ | ||
long len = Length; | ||
if (positionBefore + destination.Length > len) | ||
{ | ||
destination = positionBefore <= len ? | ||
destination.Slice(0, (int)(len - positionBefore)) : | ||
default; | ||
} | ||
|
||
// When using overlapped IO, the OS is not supposed to | ||
// touch the file pointer location at all. We will adjust it | ||
// ourselves, but only in memory. This isn't threadsafe. | ||
_filePosition += destination.Length; | ||
|
||
// We know for sure that there is nothing to read, so we just return here and avoid a sys-call. | ||
if (destination.IsEmpty && LengthCachingSupported) | ||
{ | ||
return ValueTask.FromResult(0); | ||
} | ||
// We know for sure that the file length can be safely cached and it has already been obtained. | ||
// If we have reached EOF we just return here and avoid a sys-call. | ||
return ValueTask.FromResult(0); | ||
} | ||
|
||
(SafeFileHandle.OverlappedValueTaskSource? vts, int errorCode) = RandomAccess.QueueAsyncReadFile(_fileHandle, destination, positionBefore, cancellationToken); | ||
// This implementation updates the file position before the operation starts and updates it after incomplete read. | ||
// This is done to keep backward compatibility for concurrent reads. | ||
// It uses Interlocked as there can be multiple concurrent incomplete reads updating position at the same time. | ||
long readOffset = Interlocked.Add(ref _filePosition, destination.Length) - destination.Length; | ||
|
||
(SafeFileHandle.OverlappedValueTaskSource? vts, int errorCode) = RandomAccess.QueueAsyncReadFile(_fileHandle, destination, readOffset, cancellationToken, this); | ||
return vts != null | ||
? new ValueTask<int>(vts, vts.Version) | ||
: (errorCode == 0) ? ValueTask.FromResult(0) : ValueTask.FromException<int>(HandleIOError(positionBefore, errorCode)); | ||
: (errorCode == 0) ? ValueTask.FromResult(0) : ValueTask.FromException<int>(HandleIOError(readOffset, errorCode)); | ||
} | ||
|
||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) | ||
|
@@ -69,35 +58,22 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati | |
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) | ||
=> WriteAsyncInternal(buffer, cancellationToken); | ||
|
||
private unsafe ValueTask WriteAsyncInternal(ReadOnlyMemory<byte> source, CancellationToken cancellationToken) | ||
private ValueTask WriteAsyncInternal(ReadOnlyMemory<byte> source, CancellationToken cancellationToken) | ||
{ | ||
if (!CanWrite) | ||
{ | ||
ThrowHelper.ThrowNotSupportedException_UnwritableStream(); | ||
} | ||
|
||
long positionBefore = _filePosition; | ||
if (CanSeek) | ||
{ | ||
// When using overlapped IO, the OS is not supposed to | ||
// touch the file pointer location at all. We will adjust it | ||
// ourselves, but only in memory. This isn't threadsafe. | ||
_filePosition += source.Length; | ||
UpdateLengthOnChangePosition(); | ||
} | ||
long writeOffset = CanSeek ? Interlocked.Add(ref _filePosition, source.Length) - source.Length : -1; | ||
|
||
(SafeFileHandle.OverlappedValueTaskSource? vts, int errorCode) = RandomAccess.QueueAsyncWriteFile(_fileHandle, source, positionBefore, cancellationToken); | ||
(SafeFileHandle.OverlappedValueTaskSource? vts, int errorCode) = RandomAccess.QueueAsyncWriteFile(_fileHandle, source, writeOffset, cancellationToken); | ||
return vts != null | ||
? new ValueTask(vts, vts.Version) | ||
: (errorCode == 0) ? ValueTask.CompletedTask : ValueTask.FromException(HandleIOError(positionBefore, errorCode)); | ||
: (errorCode == 0) ? ValueTask.CompletedTask : ValueTask.FromException(HandleIOError(writeOffset, errorCode)); | ||
} | ||
|
||
private Exception HandleIOError(long positionBefore, int errorCode) | ||
{ | ||
if (!_fileHandle.IsClosed && CanSeek) | ||
if (_fileHandle.CanSeek) | ||
{ | ||
// Update Position... it could be anywhere. | ||
_filePosition = positionBefore; | ||
Interlocked.Exchange(ref _filePosition, positionBefore); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is precarious regardless of the interlocked, e.g. if someone did kick off concurrent operations, they could all fight to reset this to their starting position. It's not clear there's a "right" answer. |
||
|
||
return SafeFileHandle.OverlappedValueTaskSource.GetIOError(errorCode, _fileHandle.Path); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,9 +15,9 @@ internal abstract class OSFileStreamStrategy : FileStreamStrategy | |
private readonly FileAccess _access; // What file was opened for. | ||
|
||
protected long _filePosition; | ||
protected long _length = -1; // negative means that hasn't been fetched. | ||
private long _appendStart; // When appending, prevent overwriting file. | ||
private long _length = -1; // When the file is locked for writes on Windows ((share & FileShare.Write) == 0) cache file length in-memory, negative means that hasn't been fetched. | ||
private bool _lengthCanBeCached; // SafeFileHandle hasn't been exposed and FileShare.Write was not specified when the handle was opened. | ||
private bool _lengthCanBeCached; // SafeFileHandle hasn't been exposed, file has been opened for reading and not shared for writing. | ||
|
||
internal OSFileStreamStrategy(SafeFileHandle handle, FileAccess access) | ||
{ | ||
|
@@ -44,7 +44,7 @@ internal OSFileStreamStrategy(string path, FileMode mode, FileAccess access, Fil | |
string fullPath = Path.GetFullPath(path); | ||
|
||
_access = access; | ||
_lengthCanBeCached = (share & FileShare.Write) == 0; | ||
_lengthCanBeCached = (share & FileShare.Write) == 0 && (access & FileAccess.Write) == 0; | ||
|
||
_fileHandle = SafeFileHandle.Open(fullPath, mode, access, share, options, preallocationSize); | ||
|
||
|
@@ -96,21 +96,9 @@ public unsafe sealed override long Length | |
} | ||
} | ||
|
||
protected void UpdateLengthOnChangePosition() | ||
{ | ||
// Do not update the cached length if the file is not locked | ||
// or if the length hasn't been fetched. | ||
if (!LengthCachingSupported || _length < 0) | ||
{ | ||
Debug.Assert(_length < 0); | ||
return; | ||
} | ||
|
||
if (_filePosition > _length) | ||
{ | ||
_length = _filePosition; | ||
} | ||
} | ||
// in case of concurrent incomplete reads, there can be multiple threads trying to update the position | ||
// at the same time. That is why we are using Interlocked here. | ||
internal void OnIncompleteRead(int expectedBytesRead, int actualBytesRead) => Interlocked.Add(ref _filePosition, actualBytesRead - expectedBytesRead); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @stephentoub I've used _filePosition = Math.Min(Length, _filePosition - expectedBytesRead + actualBytesRead); |
||
|
||
protected bool LengthCachingSupported => OperatingSystem.IsWindows() && _lengthCanBeCached; | ||
|
||
|
@@ -287,18 +275,8 @@ public sealed override void Write(ReadOnlySpan<byte> buffer) | |
ThrowHelper.ThrowNotSupportedException_UnwritableStream(); | ||
} | ||
|
||
try | ||
{ | ||
RandomAccess.WriteAtOffset(_fileHandle, buffer, _filePosition); | ||
} | ||
catch | ||
{ | ||
_length = -1; // invalidate cached length | ||
throw; | ||
} | ||
|
||
RandomAccess.WriteAtOffset(_fileHandle, buffer, _filePosition); | ||
_filePosition += buffer.Length; | ||
UpdateLengthOnChangePosition(); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: this function is initializing errorCode to '0' but then checking against 'ERROR_SUCCESS'. Style-wise, it'd be nice if they were consistent.