Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions libs/host/Configuration/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -575,11 +575,13 @@ internal sealed class Options : ICloneable
[Option("index-resize-threshold", Required = false, HelpText = "Hash-index Overflow bucket count over total index size in percentage to trigger index resize")]
public int IndexResizeThreshold { get; set; }

// ValueOverflowThreshold must be at least 64 bytes and strictly less than PageSize (both after rounding down to the previous power of 2).
// Validated at server-options consumption time; see GarnetServerOptions.ValueOverflowThresholdBytes.
[MemorySizeValidation(isRequired: false)]
[Option("value-overflow-threshold", Required = false, HelpText = "Max size of a value stored inline in the main-log page (larger values overflow to the heap). Accepts a memory size (e.g. 4k, 1m). Minimum 64 bytes; must be less than PageSize.")]
public string ValueOverflowThreshold { get; set; }
[Option("max-inline-key-size", Required = false, HelpText = "Maximum size of a key stored inline in the in-memory portion of the main log. Accepts a memory size (e.g. \"1k\", \"128\"). Must be in range [0, 1022] bytes; default is 1022.")]
public string MaxInlineKeySize { get; set; }

[MemorySizeValidation(isRequired: false)]
[Option("max-inline-value-size", Required = false, HelpText = "Maximum size of a value stored inline in the in-memory portion of the main log. Accepts a memory size (e.g. \"4k\", \"15m\"). Must be in range [0, 16777214] bytes; default is min (1m, PageSize / 2).")]
public string MaxInlineValueSize { get; set; }

[MemorySizeValidation(isRequired: false)]
[Option("initial-io-record-size", Required = false, HelpText = "Initial IO read size for records on disk. Accepts a memory size (e.g. 4k, 8k). Default is 128 bytes.")]
Expand Down Expand Up @@ -951,7 +953,8 @@ endpoint is IPEndPoint listenEp && clusterAnnounceEndpoint[0] is IPEndPoint anno
ExtensionAllowUnsignedAssemblies = ExtensionAllowUnsignedAssemblies.GetValueOrDefault(),
IndexResizeFrequencySecs = IndexResizeFrequencySecs,
IndexResizeThreshold = IndexResizeThreshold,
ValueOverflowThreshold = ValueOverflowThreshold,
MaxInlineKeySize = MaxInlineKeySize,
MaxInlineValueSize = MaxInlineValueSize,
InitialIORecordSize = InitialIORecordSize,
LoadModuleCS = LoadModuleCS,
FailOnRecoveryError = FailOnRecoveryError.GetValueOrDefault(),
Expand Down
7 changes: 5 additions & 2 deletions libs/host/defaults.conf
Original file line number Diff line number Diff line change
Expand Up @@ -414,8 +414,11 @@
/* Overflow bucket count over total index size in percentage to trigger index resize */
"IndexResizeThreshold": 50,

/* Max size of a value stored inline in the main-log page (larger values overflow to the heap). Accepts a memory size (e.g. "4k", "1m"). Minimum 64 bytes; must be less than PageSize. */
"ValueOverflowThreshold": "16k",
/* Maximum size of a key stored inline in the in-memory portion of the main log. Accepts a memory size (e.g. "1k", "128"). Must be in range [0, 1022] bytes; default is 1022. */
"MaxInlineKeySize": null,

/* Maximum size of a value stored inline in the in-memory portion of the main log. Accepts a memory size (e.g. "4k", "15m"). Must be in range [0, 16777214] bytes; default is min (1m, PageSize / 2). */
"MaxInlineValueSize": null,

/* Initial IO read size for records on disk. Accepts a memory size (e.g. "4k", "8k"). null means use the default (128 bytes). */
"InitialIORecordSize": null,
Expand Down
94 changes: 55 additions & 39 deletions libs/server/Servers/GarnetServerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,16 @@ public class GarnetServerOptions : ServerOptions
public int IndexResizeThreshold = 50;

/// <summary>
/// The size at which a value string becomes an overflow byte[]. Accepts bytes or k/m/g suffixes (e.g. "4k", "1m").
/// Valid range: 64 bytes to 256m. Rounds down to previous power of 2 by Tsavorite.
/// Maximum size of a key stored inline in the in-memory portion of the main log.
/// Accepts a memory size (e.g. \"1k\", \"128\"). Must be in range [0, 1022] bytes; default is 1022.
/// </summary>
public string ValueOverflowThreshold = "16k";
public string MaxInlineKeySize = null;

/// <summary>
/// Maximum size of a value stored inline in the in-memory portion of the main log.
/// Accepts a memory size (e.g. \"4k\", \"15m\"). Must be in range [0, 16777214] bytes; default is min (1m, PageSize / 2).
/// </summary>
public string MaxInlineValueSize = null;

/// <summary>
/// Initial IO read size for records on disk. Accepts bytes or k/m/g suffixes (e.g. "4k", "8k").
Expand Down Expand Up @@ -634,15 +640,17 @@ public KVSettings GetSettings(ILoggerFactory loggerFactory, LightEpoch epoch, St

var indexCacheLines = IndexSizeCachelines("hash index size", IndexMemorySize);

var pageSize = 1L << PageSizeBits();
KVSettings kvSettings = new()
{
IndexSize = indexCacheLines * 64L,
PreallocateLog = false,
MutableFraction = MutablePercent / 100.0,
PageSize = 1L << PageSizeBits(),
PageSize = pageSize,
Epoch = epoch,
StateMachineDriver = stateMachineDriver,
MaxInlineValueSize = ValueOverflowThresholdBytes(),
MaxInlineKeySize = MaxInlineKeySizeBytes(),
MaxInlineValueSize = MaxInlineValueSizeBytes(pageSize),
InitialIORecordSize = GetInitialIORecordSizeBytes(),
loggerFactory = loggerFactory,
logger = loggerFactory?.CreateLogger("TsavoriteKV [main]")
Expand Down Expand Up @@ -842,47 +850,55 @@ public int MemorySizeBits(string memorySize)
internal int ReadCachePageSizeBits() => ValidatedPageSizeBits(ReadCachePageSize, nameof(ReadCachePageSize));

/// <summary>
/// Parse and validate <see cref="ValueOverflowThreshold"/> as a byte count.
/// Tsavorite requires this to be at least 64 bytes and at most 0xFFFFFE (the RecordDataHeader value-length field's
/// inline limit; see <c>LogSettings.MaxInlineValueSizeLimit</c> — this value MUST be kept in sync because LogSettings
/// is internal to Tsavorite.core and not visible from Garnet.server).
/// The value will be used directly.
/// Additionally, the effective value must be strictly less than the effective PageSize
/// so that a value of this size, plus per-record overhead, can be allocated within a single page;
/// if not, it is clamped down to the largest valid value (with a warning).
/// Parse <see cref="MaxInlineKeySize"/> as a byte count.
/// Returns the default value (1022 bytes, the Tsavorite limit) if the value is null or empty.
/// </summary>
/// <returns>The byte value used for <c>KVSettings.MaxInlineValueSize</c>.</returns>
/// <returns>The byte-length value used for <c>KVSettings.MaxInlineKeySize</c>.</returns>
/// <exception cref="Exception">Thrown when the value cannot be parsed or is outside the allowed range.</exception>
public int MaxInlineKeySizeBytes()
{
const long MinBytes = 0; // TODO: LogSettings.MinMaxInlineSize
const long MaxBytes = 1022; // TODO: LogSettings.MaxInlineKeySizeLimit (= (1 << kKeyLengthBits) - 2)

if (string.IsNullOrEmpty(MaxInlineKeySize))
return KVSettings.DefaultMaxInlineKeySize;

if (!TryParseSize(MaxInlineKeySize, out var sizeInBytes))
throw new Exception($"Unable to parse {nameof(MaxInlineKeySize)} value '{MaxInlineKeySize}'. Expected a memory size string (e.g. '1k', '128').");

if (sizeInBytes < MinBytes || sizeInBytes > MaxBytes)
throw new Exception($"{nameof(MaxInlineKeySize)} value '{MaxInlineKeySize}' ({sizeInBytes} bytes) is outside the allowed range [{MinBytes}, {MaxBytes}] bytes.");

return (int)sizeInBytes;
}

/// <summary>
/// Parse and validate <see cref="MaxInlineValueSize"/> as a byte count.
/// Tsavorite requires this to be at most 0xFFFFFE (the RecordDataHeader value-length field's
/// inline limit; see <c>LogSettings.MaxInlineValueSizeLimit</c> — this value MUST be kept in sync because LogSettings
/// is internal to Tsavorite.core and not visible from Garnet.server). If this value is not specified, it defaults
/// to the minimum of 1m or <paramref name="pageSize"/> / 2; otherwise, the value must be &lt;= pageSize / 2.
/// <returns>The byte-length value used for <c>KVSettings.MaxInlineValueSize</c>.</returns>
/// <exception cref="Exception">Thrown when the value cannot be parsed or is outside the allowed byte range.</exception>
public int ValueOverflowThresholdBytes()
/// </summary>
public int MaxInlineValueSizeBytes(long pageSize)
{
const long MinBytes = 64L; // LogSettings.MinMaxInlineSize
const long MaxBytes = 0xFFFFFE; // LogSettings.MaxInlineValueSizeLimit (= (1 << kValueLengthBits) - 2)
const long MinBytes = 0; // TODO: LogSettings.MinMaxInlineSize
const long MaxBytes = 0xFFFFFE; // TODO: LogSettings.MaxInlineValueSizeLimit (= (1 << kValueLengthBits) - 2)

if (string.IsNullOrEmpty(ValueOverflowThreshold))
throw new Exception($"{nameof(ValueOverflowThreshold)} must be specified");
if (string.IsNullOrEmpty(MaxInlineValueSize))
return (int)Math.Min(pageSize / 2, KVSettings.DefaultMaxInlineValueSize);

if (!TryParseSize(ValueOverflowThreshold, out var sizeInBytes))
throw new Exception($"Unable to parse {nameof(ValueOverflowThreshold)} value '{ValueOverflowThreshold}'. Expected a memory size string (e.g. '4k', '1m').");
if (!TryParseSize(MaxInlineValueSize, out var sizeInBytes))
throw new Exception($"Unable to parse {nameof(MaxInlineValueSize)} value '{MaxInlineValueSize}'. Expected a memory size string (e.g. '4k', '16m').");

if (sizeInBytes < MinBytes || sizeInBytes > MaxBytes)
throw new Exception($"{nameof(ValueOverflowThreshold)} value '{ValueOverflowThreshold}' ({sizeInBytes} bytes) is outside the allowed range [{MinBytes}, {MaxBytes}] bytes.");

// Cross-property check: a value of MaxInlineValueSize plus per-record overhead must fit on a page.
// Both PageSize and MaxInlineValueSize are rounded down to the previous power of 2 by Tsavorite,
// so we require effectiveValue < effectivePage (i.e., value bits < page bits), which guarantees the
// value occupies at most half the page and leaves room for the record header, key, and optional fields.
// If not satisfied (e.g. defaults combined with an unusually small PageSize), clamp down with a warning
// rather than failing — this preserves the "rounds down silently" behavior of other size settings.
var valueBits = (int)Math.Log(PreviousPowerOf2(sizeInBytes), 2);
var pageBits = PageSizeBits();
if (valueBits >= pageBits)
{
var clampedBits = pageBits - 1;
var clampedBytes = 1L << clampedBits;
logger?.LogWarning("Warning: clamping {Name} '{Value}' (effective {EffectiveValue} bytes) down to {Clamped} bytes so it fits within PageSize '{Page}' (effective {EffectivePage} bytes).",
nameof(ValueOverflowThreshold), ValueOverflowThreshold, 1L << valueBits, clampedBytes, PageSize, 1L << pageBits);
return (int)clampedBytes;
}
throw new Exception($"{nameof(MaxInlineValueSize)} value '{MaxInlineValueSize}' ({sizeInBytes} bytes) is outside the allowed range [{MinBytes}, {MaxBytes}] bytes.");

// This check guarantees at least one record fits on a page, because the minimum page size is 4k, and 2k is larger than
// the PageHeader plus non-value components of a record (RecordInfo, RecordDataHeader, Key, and possible Optional fields).
if (sizeInBytes > pageSize / 2)
throw new Exception($"{nameof(MaxInlineValueSize)} value '{MaxInlineValueSize}' ({sizeInBytes} bytes) is greater than half the page size ({pageSize / 2} bytes for PageSize {pageSize} bytes).");

return (int)sizeInBytes;
}
Expand Down
16 changes: 13 additions & 3 deletions libs/storage/Tsavorite/cs/src/core/Index/Common/KVSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,24 @@ public sealed class KVSettings : IDisposable
public StateMachineDriver StateMachineDriver = null;

/// <summary>
/// Maximum size of a key stored inline in the in-memory portion of the main log for both allocators.
/// Default maximum size of a key stored inline in the in-memory portion of the main log; this is the maximum inline size for <see cref="RecordDataHeader.KeyLength"/>.
/// </summary>
public int MaxInlineKeySize = LogSettings.DefaultMaxInlineKeySize;
public const int DefaultMaxInlineKeySize = 1022;

/// <summary>
/// Maximum size of a key stored inline in the in-memory portion of the main log.
/// </summary>
public int MaxInlineKeySize = DefaultMaxInlineKeySize;
Comment thread
TedHartMS marked this conversation as resolved.

/// <summary>
/// Default maximum size of a value stored inline in the in-memory portion of the main log for both allocators; this is less than the maximum inline size for <see cref="RecordDataHeader.ValueLength"/>.
/// </summary>
public const int DefaultMaxInlineValueSize = 1024 * 1024;

/// <summary>
/// Maximum size of a value stored inline in the in-memory portion of the main log for <see cref="SpanByteAllocator{TStoreFunctions}"/>.
/// </summary>
public int MaxInlineValueSize = LogSettings.DefaultMaxInlineValueSize;
public int MaxInlineValueSize = DefaultMaxInlineValueSize;

/// <summary>Sentinel value indicating that the default <see cref="IStreamBuffer.DefaultInitialIORecordSize"/> should be used.</summary>
public const int UseDefaultInitialIORecordSize = -1;
Expand Down
Loading
Loading