blob: c76d584bed7a45628b2ad5537c7f111b3a5dbabc [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
using System.Buffers;
using OpenDAL.Interop.NativeObject;
using OpenDAL.Interop.Result;
namespace OpenDAL.Interop.Buffers;
/// <summary>
/// Native <see cref="IBufferWriter{T}"/> whose contents transfer to the
/// OpenDAL layer when a write consumes them, without copying.
/// </summary>
/// <remarks>
/// Allocate with <see cref="Operator.AllocateWriteBuffer"/>, produce through the
/// standard <see cref="IBufferWriter{T}"/> contract (for example with
/// <c>Utf8JsonWriter</c>), then hand it to a write. The write consumes the
/// buffer: every later access throws. Growth adds native segments that never
/// move, so earlier memory stays valid until <see cref="Dispose"/>. Instances
/// are not thread-safe; disposing an unconsumed buffer returns the memory.
/// </remarks>
internal sealed class WriteBuffer : IBufferWriter<byte>, IDisposable
{
internal const int DefaultInitialCapacity = 64 * 1024; // 64 KiB
// New segments double the previous capacity up to this bound, mirroring
// ArrayBufferWriter's growth curve while keeping segments coarse enough
// that a large payload stays a handful of chunks.
private const int MaxSegmentCapacity = 8 * 1024 * 1024; // 8 MiB
private IntPtr handle;
private IntPtr data;
private int capacity;
private int tailWritten;
private long totalWritten;
private bool consumed;
private NativeMemoryManager? memoryManager;
internal WriteBuffer(IntPtr handle, IntPtr data, int capacity)
{
this.handle = handle;
this.data = data;
this.capacity = capacity;
}
/// <summary>
/// Gets the total number of bytes committed with <see cref="Advance"/>.
/// </summary>
public long WrittenCount => totalWritten;
internal IntPtr Handle
{
get
{
ThrowIfNotUsable();
return handle;
}
}
internal int TailWritten => tailWritten;
internal void MarkConsumed()
{
consumed = true;
}
/// <inheritdoc />
/// <exception cref="ObjectDisposedException">The buffer has been disposed.</exception>
/// <exception cref="InvalidOperationException">The buffer contents were already consumed by a write.</exception>
/// <exception cref="OpenDALException">Growing the buffer fails natively.</exception>
public unsafe Span<byte> GetSpan(int sizeHint = 0)
{
EnsureCapacity(sizeHint);
return new Span<byte>((byte*)data + tailWritten, capacity - tailWritten);
}
/// <inheritdoc />
/// <exception cref="ObjectDisposedException">The buffer has been disposed.</exception>
/// <exception cref="InvalidOperationException">The buffer contents were already consumed by a write.</exception>
/// <exception cref="OpenDALException">Growing the buffer fails natively.</exception>
public unsafe Memory<byte> GetMemory(int sizeHint = 0)
{
EnsureCapacity(sizeHint);
memoryManager ??= new NativeMemoryManager((byte*)data, capacity);
return memoryManager.Memory[tailWritten..];
}
/// <inheritdoc />
/// <exception cref="ObjectDisposedException">The buffer has been disposed.</exception>
/// <exception cref="InvalidOperationException">The buffer contents were already consumed by a write.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="count"/> is negative or exceeds the last acquired span.
/// </exception>
public void Advance(int count)
{
ThrowIfNotUsable();
if (count < 0 || count > capacity - tailWritten)
{
throw new ArgumentOutOfRangeException(nameof(count), count,
"Advance count must be between 0 and the size of the last acquired buffer.");
}
tailWritten += count;
totalWritten += count;
}
private void EnsureCapacity(int sizeHint)
{
ThrowIfNotUsable();
ArgumentOutOfRangeException.ThrowIfNegative(sizeHint);
var needed = sizeHint == 0 ? 1 : sizeHint;
if (capacity - tailWritten >= needed)
{
return;
}
var nextCapacity = (int)Math.Max(needed, Math.Min((long)capacity * 2, MaxSegmentCapacity));
var result = NativeMethods.write_buffer_add_segment(handle, (nuint)tailWritten, (nuint)nextCapacity);
var segment = Operator.ToValueOrThrowAndRelease<OpenDALWriteBuffer, OpenDALWriteBufferResult>(result);
data = segment.Data;
capacity = checked((int)segment.Capacity);
tailWritten = 0;
// The old manager's memory stays valid because sealed segments never
// move; the new segment simply needs its own view.
memoryManager = null;
}
private void ThrowIfNotUsable()
{
ObjectDisposedException.ThrowIf(handle == IntPtr.Zero, this);
if (consumed)
{
throw new InvalidOperationException(
"The write buffer contents were already consumed by a write; they now belong to the native layer. Allocate a new buffer for the next write.");
}
}
public void Dispose()
{
if (handle != IntPtr.Zero)
{
NativeMethods.write_buffer_free(handle);
handle = IntPtr.Zero;
data = IntPtr.Zero;
memoryManager = null;
}
}
}