blob: 32168047c7e9ceccd3ea68a0f5d22cb3144f043f [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;
using System.Threading;
using System.Threading.Tasks;
namespace Apache.Arrow.Adbc.Drivers.Databricks.Reader.CloudFetch
{
/// <summary>
/// Manages memory allocation for prefetched files.
///
/// This manager tracks in-flight download memory based on the compressed file sizes
/// received from the server. It does NOT track the decompressed data size, as that
/// memory is naturally bounded by the result queue capacity and batch processing flow.
///
/// The memory limit controls how many concurrent downloads can be in-flight at once,
/// preventing unbounded parallel downloads from exhausting system resources.
/// </summary>
internal sealed class CloudFetchMemoryBufferManager : ICloudFetchMemoryBufferManager
{
private const int DefaultMemoryBufferSizeMB = 200;
private readonly long _maxMemory;
private long _usedMemory;
private readonly SemaphoreSlim _memorySemaphore;
/// <summary>
/// Initializes a new instance of the <see cref="CloudFetchMemoryBufferManager"/> class.
/// </summary>
/// <param name="maxMemoryMB">The maximum memory allowed for buffering in megabytes.</param>
public CloudFetchMemoryBufferManager(int? maxMemoryMB = null)
{
int memoryMB = maxMemoryMB ?? DefaultMemoryBufferSizeMB;
if (memoryMB <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maxMemoryMB), "Memory buffer size must be positive.");
}
// Convert MB to bytes
_maxMemory = memoryMB * 1024L * 1024L;
_usedMemory = 0;
_memorySemaphore = new SemaphoreSlim(1, 1);
}
/// <inheritdoc />
public long MaxMemory => _maxMemory;
/// <inheritdoc />
public long UsedMemory => Interlocked.Read(ref _usedMemory);
/// <inheritdoc />
public bool TryAcquireMemory(long size)
{
if (size <= 0)
{
throw new ArgumentOutOfRangeException(nameof(size), "Size must be positive.");
}
// Try to acquire memory (size is the compressed file size from the server)
long originalValue;
long newValue;
do
{
originalValue = Interlocked.Read(ref _usedMemory);
newValue = originalValue + size;
// Check if we would exceed the maximum memory
if (newValue > _maxMemory)
{
return false;
}
}
while (Interlocked.CompareExchange(ref _usedMemory, newValue, originalValue) != originalValue);
return true;
}
/// <inheritdoc />
public async Task AcquireMemoryAsync(long size, CancellationToken cancellationToken)
{
if (size <= 0)
{
throw new ArgumentOutOfRangeException(nameof(size), "Size must be positive.");
}
// Special case: if size is greater than max memory, we'll never be able to acquire it
// Note: size is the compressed file size from the server
if (size > _maxMemory)
{
throw new ArgumentOutOfRangeException(nameof(size), $"Requested size ({size} bytes) exceeds maximum memory ({_maxMemory} bytes).");
}
while (!cancellationToken.IsCancellationRequested)
{
// Try to acquire memory without blocking
if (TryAcquireMemory(size))
{
return;
}
// If we couldn't acquire memory, wait for some to be released
await Task.Delay(10, cancellationToken).ConfigureAwait(false);
}
// If we get here, cancellation was requested
cancellationToken.ThrowIfCancellationRequested();
}
/// <inheritdoc />
public void ReleaseMemory(long size)
{
if (size <= 0)
{
throw new ArgumentOutOfRangeException(nameof(size), "Size must be positive.");
}
// Release memory
long newValue = Interlocked.Add(ref _usedMemory, -size);
// Ensure we don't go negative
if (newValue < 0)
{
// This should never happen if the code is correct
Interlocked.Exchange(ref _usedMemory, 0);
throw new InvalidOperationException("Memory buffer manager released more memory than was acquired.");
}
}
}
}