add all
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
|
||||
namespace Best.HTTP.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// A builder struct for constructing an instance of the HTTPCache class with optional configuration options and callbacks.
|
||||
/// </summary>
|
||||
public struct HTTPCacheBuilder
|
||||
{
|
||||
private HTTPCacheOptions _options;
|
||||
private OnBeforeBeginCacheDelegate _callback;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the configuration options for the HTTP cache.
|
||||
/// </summary>
|
||||
/// <param name="options">The <see cref="HTTPCacheOptions"/> containing cache configuration settings.</param>
|
||||
/// <returns>The current <see cref="HTTPCacheBuilder"/> instance for method chaining.</returns>
|
||||
public HTTPCacheBuilder WithOptions(HTTPCacheOptions options)
|
||||
{
|
||||
this._options = options;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the configuration options for the HTTP cache using an <see cref="HTTPCacheOptionsBuilder"/>.
|
||||
/// </summary>
|
||||
/// <param name="optionsBuilder">An <see cref="HTTPCacheOptionsBuilder"/> for building cache configuration settings.</param>
|
||||
/// <returns>The current <see cref="HTTPCacheBuilder"/> instance for method chaining.</returns>
|
||||
public HTTPCacheBuilder WithOptions(HTTPCacheOptionsBuilder optionsBuilder)
|
||||
{
|
||||
this._options = optionsBuilder.Build();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a callback delegate to be executed before caching of an entity begins.
|
||||
/// </summary>
|
||||
/// <param name="callback">The delegate to be executed before caching starts.</param>
|
||||
/// <returns>The current <see cref="HTTPCacheBuilder"/> instance for method chaining.</returns>
|
||||
public HTTPCacheBuilder WithBeforeBeginCacheCallback(OnBeforeBeginCacheDelegate callback)
|
||||
{
|
||||
this._callback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds and returns an instance of the <see cref="HTTPCache"/> with the specified configuration options and callback delegate.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="HTTPCache"/> instance configured with the specified options and callback.</returns>
|
||||
public HTTPCache Build()
|
||||
=> new HTTPCache(this._options) { OnBeforeBeginCache = this._callback };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A builder struct for constructing an instance of <see cref="HTTPCacheOptions"/> with optional configuration settings.
|
||||
/// </summary>
|
||||
public struct HTTPCacheOptionsBuilder
|
||||
{
|
||||
private HTTPCacheOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum cache size for the HTTP cache.
|
||||
/// </summary>
|
||||
/// <param name="maxCacheSize">The maximum size, in bytes, that the cache can reach.</param>
|
||||
/// <returns>The current <see cref="HTTPCacheOptionsBuilder"/> instance for method chaining.</returns>
|
||||
public HTTPCacheOptionsBuilder WithMaxCacheSize(ulong maxCacheSize)
|
||||
{
|
||||
this._options = this._options ?? new HTTPCacheOptions();
|
||||
this._options.MaxCacheSize = maxCacheSize;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum duration for which cached entries will be retained.
|
||||
/// By default all entities (even stalled ones) are kept cached until they are evicted to make room for new, fresh ones.
|
||||
/// </summary>
|
||||
/// <param name="olderThan">The maximum age for cached entries to be retained.</param>
|
||||
/// <returns>The current <see cref="HTTPCacheOptionsBuilder"/> instance for method chaining.</returns>
|
||||
public HTTPCacheOptionsBuilder WithDeleteOlderThen(TimeSpan olderThan)
|
||||
{
|
||||
this._options = this._options ?? new HTTPCacheOptions();
|
||||
this._options.DeleteOlder = olderThan;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds and returns an instance of <see cref="HTTPCacheOptions"/> with the specified configuration settings.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="HTTPCacheOptions"/> instance configured with the specified settings.</returns>
|
||||
public HTTPCacheOptions Build()
|
||||
=> this._options ?? new HTTPCacheOptions();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 114a3d036d69b7c4ca732b2f6a48b292
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1090
Packages/com.tivadar.best.http/Runtime/HTTP/Caching/HTTPCache.cs
Normal file
1090
Packages/com.tivadar.best.http/Runtime/HTTP/Caching/HTTPCache.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 202cffdfebb3b8e4ba485c44e5f5b1ce
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Best.HTTP.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a writer for caching HTTP response content.
|
||||
/// </summary>
|
||||
public class HTTPCacheContentWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the parent HTTPCache instance associated with this content writer.
|
||||
/// </summary>
|
||||
public HTTPCache Cache { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash identifying the resource. If <see cref="Write(BufferSegment)"/> fails, it becomes an invalid one.
|
||||
/// </summary>
|
||||
public Hash128 Hash { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Expected length of the content. Has a non-zero value only when the server is sending a "content-length" header.
|
||||
/// </summary>
|
||||
public ulong ExpectedLength { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of bytes written to the cache.
|
||||
/// </summary>
|
||||
public ulong ProcessedLength { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Context of this cache writer used for logging.
|
||||
/// </summary>
|
||||
public LoggingContext Context { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Underlying stream the download bytes are written into.
|
||||
/// </summary>
|
||||
private Stream _contentStream;
|
||||
|
||||
internal HTTPCacheContentWriter(HTTPCache cache, Hash128 hash, Stream contentStream, ulong expectedLength, LoggingContext loggingContext)
|
||||
{
|
||||
this.Cache = cache;
|
||||
this.Hash = hash;
|
||||
this._contentStream = contentStream;
|
||||
this.ExpectedLength = expectedLength;
|
||||
this.Context = loggingContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes content to the underlying stream.
|
||||
/// </summary>
|
||||
/// <param name="segment"><see cref="BufferSegment"/> holding a reference to the data and containing information about the offset and count of the valid range of data.</param>
|
||||
public void Write(BufferSegment segment)
|
||||
{
|
||||
if (!this.Hash.isValid)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
this._contentStream?.Write(segment.Data, segment.Offset, segment.Count);
|
||||
this.ProcessedLength += (ulong)segment.Count;
|
||||
|
||||
if (this.ProcessedLength > this.ExpectedLength)
|
||||
{
|
||||
if (!this.Cache.IsThereEnoughSpaceAfterMaintain(this.ProcessedLength, this.Context))
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(HTTPCacheContentWriter), $"Not enough space({this.ProcessedLength:N0}) in cache({this.Cache.CacheSize:N0}), even after Maintain!", this.Context);
|
||||
|
||||
this.Cache?.EndCache(this, false, this.Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Warning(nameof(HTTPCacheContentWriter), $"{nameof(Write)}({segment}): {ex}", this.Context);
|
||||
|
||||
// EndCache will call Close, we don't have to in this catch block
|
||||
this.Cache?.EndCache(this, false, this.Context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close the underlying stream and invalidate the hash.
|
||||
/// </summary>
|
||||
internal void Close()
|
||||
{
|
||||
this._contentStream?.Close();
|
||||
this._contentStream = null;
|
||||
|
||||
// this will set an invalid cache, further HTTPCaching.EndCache calls will return early.
|
||||
this.Hash = new Hash128();
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{nameof(HTTPCacheContentWriter)} {Hash} {ProcessedLength:N0}]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f1a3db4ad5927624182f465b44a2f2ac
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,733 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Databases;
|
||||
using Best.HTTP.Shared.Databases.Indexing;
|
||||
using Best.HTTP.Shared.Databases.Indexing.Comparers;
|
||||
using Best.HTTP.Shared.Databases.MetadataIndexFinders;
|
||||
using Best.HTTP.Shared.Databases.Utils;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
using Best.HTTP.Shared.PlatformSupport.Threading;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Best.HTTP.Caching
|
||||
{
|
||||
struct v128View
|
||||
{
|
||||
public ulong low;
|
||||
public ulong high;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible lock-states a cache-content can be in.
|
||||
/// </summary>
|
||||
public enum LockTypes : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// No reads or writes are happening on the cached content.
|
||||
/// </summary>
|
||||
Unlocked,
|
||||
|
||||
/// <summary>
|
||||
/// There's one writer operating on the cached content. No other writes or reads allowed while this lock is held on the content.
|
||||
/// </summary>
|
||||
Write,
|
||||
|
||||
/// <summary>
|
||||
/// There's at least one read operation happening on the cached content. No writes allowed while this lock is held on the content.
|
||||
/// </summary>
|
||||
Read
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata stored for every cached content. It contains only limited data about the content to help early cache decision making and cache management.
|
||||
/// </summary>
|
||||
internal class CacheMetadata : Metadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique hash of the cached content, generated by <see cref="HTTPCache.CalculateHash(HTTPMethods, Uri)"/>.
|
||||
/// </summary>
|
||||
public UnityEngine.Hash128 Hash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the stored content in bytes.
|
||||
/// </summary>
|
||||
public ulong ContentLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the last time the content is accessed. Also initialized when the initial download completes.
|
||||
/// </summary>
|
||||
public DateTime LastAccessTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// What kind of lock the content is currently in.
|
||||
/// </summary>
|
||||
public LockTypes Lock { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of readers.
|
||||
/// </summary>
|
||||
public int ReadLockCount { get; set; }
|
||||
|
||||
public unsafe override void SaveTo(Stream stream)
|
||||
{
|
||||
base.SaveTo(stream);
|
||||
|
||||
var hash = this.Hash;
|
||||
v128View view = *(v128View*)&hash;
|
||||
|
||||
stream.EncodeUnsignedVariableByteInteger(view.low);
|
||||
stream.EncodeUnsignedVariableByteInteger(view.high);
|
||||
stream.EncodeUnsignedVariableByteInteger(ContentLength);
|
||||
stream.EncodeSignedVariableByteInteger(LastAccessTime.ToBinary() >> CacheMetadataContentParser.PrecisionShift);
|
||||
|
||||
// Only Write locks should persist as Reads doesn't alter the cached content
|
||||
if (this.Lock == LockTypes.Write)
|
||||
stream.EncodeUnsignedVariableByteInteger((byte)this.Lock);
|
||||
else
|
||||
stream.EncodeUnsignedVariableByteInteger((byte)LockTypes.Unlocked);
|
||||
}
|
||||
|
||||
public unsafe override void LoadFrom(Stream stream)
|
||||
{
|
||||
base.LoadFrom(stream);
|
||||
|
||||
var hash = default(v128View);
|
||||
hash.low = stream.DecodeUnsignedVariableByteInteger();
|
||||
hash.high = stream.DecodeUnsignedVariableByteInteger();
|
||||
|
||||
this.Hash = *(UnityEngine.Hash128*)&hash;
|
||||
|
||||
this.ContentLength = stream.DecodeUnsignedVariableByteInteger();
|
||||
this.LastAccessTime = DateTime.FromBinary(stream.DecodeSignedVariableByteInteger() << CacheMetadataContentParser.PrecisionShift);
|
||||
|
||||
this.Lock = (LockTypes)stream.DecodeUnsignedVariableByteInteger();
|
||||
}
|
||||
|
||||
public override string ToString() => $"[Metadata {Hash}, {ContentLength:N0}, {Lock}, {ReadLockCount}]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible caching flags that a `Cache-Control` header can send.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum CacheFlags : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// No special treatment required.
|
||||
/// </summary>
|
||||
None = 0x00,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the entity must be revalidated with the server or can be serverd directly from the cache without touching the server when the content is considered stale.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9111.html#name-must-revalidate"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
MustRevalidate = 0x01,
|
||||
|
||||
/// <summary>
|
||||
/// If it's true, the client always have to revalidate the cached content when it's stale.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9111.html#name-no-cache-2"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
NoCache = 0x02
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cached content associated with a <see cref="CacheMetadata"/>.
|
||||
/// </summary>
|
||||
/// <remarks>This is NOT the cached content received from the server! It's for storing caching values to decide on how the content can be used.</remarks>
|
||||
internal sealed class CacheMetadataContent
|
||||
{
|
||||
/// <summary>
|
||||
/// ETag of the entity.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public string ETag;
|
||||
|
||||
/// <summary>
|
||||
/// LastModified date of the entity. Use ToString("r") to convert it to the format defined in RFC 1123.
|
||||
/// </summary>
|
||||
public DateTime LastModified = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// When the cache will expire.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9111.html#name-expires"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public DateTime Expires = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// The age that came with the response
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9111.html#name-age"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public uint Age;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum how long the entry should served from the cache without revalidation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9111.html#name-max-age-2"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public uint MaxAge;
|
||||
|
||||
/// <summary>
|
||||
/// The Date that came with the response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public DateTime Date = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// It's a grace period to serve staled content without revalidation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc5861.html#section-3"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public uint StaleWhileRevalidate;
|
||||
|
||||
/// <summary>
|
||||
/// Allows the client to serve stale content if the server responds with an 5xx error.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc5861.html#section-4"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public uint StaleIfError;
|
||||
|
||||
/// <summary>
|
||||
/// bool values packed into one single flag.
|
||||
/// </summary>
|
||||
public CacheFlags Flags = CacheFlags.None;
|
||||
|
||||
/// <summary>
|
||||
/// The value of the clock at the time of the request that resulted in the stored response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.3-3.8"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public DateTime RequestTime = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// The value of the clock at the time the response was received.
|
||||
/// </summary>
|
||||
public DateTime ResponseTime = DateTime.MinValue;
|
||||
|
||||
public CacheMetadataContent()
|
||||
{
|
||||
}
|
||||
|
||||
internal void From(Dictionary<string, List<string>> headers)
|
||||
{
|
||||
this.ETag = headers.GetFirstHeaderValue("ETag").ToStr(this.ETag ?? string.Empty);
|
||||
this.Expires = headers.GetFirstHeaderValue("Expires").ToDateTime(this.Expires);
|
||||
if (this.Expires < DateTime.UtcNow)
|
||||
this.Expires = DateTime.MinValue;
|
||||
|
||||
this.LastModified = headers.GetFirstHeaderValue("Last-Modified").ToDateTime(DateTime.MinValue);
|
||||
this.Age = headers.GetFirstHeaderValue("Age").ToUInt32(this.Age);
|
||||
this.Date = headers.GetFirstHeaderValue("Date").ToDateTime(this.Date);
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.1-4
|
||||
// When there is more than one value present for a given directive
|
||||
// (e.g., two Expires header field lines or multiple Cache-Control: max-age directives),
|
||||
// either the first occurrence should be used or the response should be considered stale.
|
||||
var cacheControl = headers.GetFirstHeaderValue("cache-control");
|
||||
if (!string.IsNullOrEmpty(cacheControl))
|
||||
{
|
||||
HeaderParser parser = new HeaderParser(cacheControl);
|
||||
|
||||
if (parser.Values != null)
|
||||
{
|
||||
this.Flags = CacheFlags.None;
|
||||
|
||||
for (int i = 0; i < parser.Values.Count; ++i)
|
||||
{
|
||||
var kvp = parser.Values[i];
|
||||
|
||||
switch (kvp.Key.ToLowerInvariant())
|
||||
{
|
||||
// https://www.rfc-editor.org/rfc/rfc9111.html#name-max-age-2
|
||||
case "max-age":
|
||||
if (kvp.HasValue)
|
||||
{
|
||||
// Some cache proxies will return float values
|
||||
double maxAge;
|
||||
if (double.TryParse(kvp.Value, out maxAge) && maxAge >= 0)
|
||||
this.MaxAge = (uint)maxAge;
|
||||
else
|
||||
this.MaxAge = 0;
|
||||
}
|
||||
else
|
||||
this.MaxAge = 0;
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3-8
|
||||
// https://www.rfc-editor.org/rfc/rfc9111.html#cache-response-directive.s-maxage
|
||||
// If a response includes a Cache-Control header field with the max-age directive (Section 5.2.2.1), a recipient MUST ignore the Expires header field.
|
||||
this.Expires = DateTime.MinValue;
|
||||
break;
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9111.html#name-must-revalidate
|
||||
case "must-revalidate": this.Flags |= CacheFlags.MustRevalidate; break;
|
||||
// https://www.rfc-editor.org/rfc/rfc9111.html#name-no-cache-2
|
||||
case "no-cache": this.Flags |= CacheFlags.NoCache; break;
|
||||
// https://www.rfc-editor.org/rfc/rfc5861.html#section-3
|
||||
case "stale-while-revalidate": this.StaleWhileRevalidate = kvp.HasValue ? kvp.Value.ToUInt32(0) : 0; break;
|
||||
// https://www.rfc-editor.org/rfc/rfc5861.html#section-4
|
||||
case "stale-if-error": this.StaleIfError = kvp.HasValue ? kvp.Value.ToUInt32(0) : 0; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CacheMetadataContentParser : IDiskContentParser<CacheMetadataContent>
|
||||
{
|
||||
public const int PrecisionShift = 24;
|
||||
|
||||
public void Encode(Stream stream, CacheMetadataContent content)
|
||||
{
|
||||
stream.EncodeUnsignedVariableByteInteger(content.MaxAge);
|
||||
stream.WriteLengthPrefixedString(content.ETag);
|
||||
stream.EncodeSignedVariableByteInteger(content.LastModified.ToBinary() >> PrecisionShift);
|
||||
stream.EncodeSignedVariableByteInteger(content.Expires.ToBinary() >> PrecisionShift);
|
||||
stream.EncodeUnsignedVariableByteInteger(content.Age);
|
||||
stream.EncodeSignedVariableByteInteger(content.Date.ToBinary() >> PrecisionShift);
|
||||
stream.EncodeUnsignedVariableByteInteger((byte)content.Flags);
|
||||
stream.EncodeUnsignedVariableByteInteger(content.StaleWhileRevalidate);
|
||||
stream.EncodeUnsignedVariableByteInteger(content.StaleIfError);
|
||||
stream.EncodeSignedVariableByteInteger(content.RequestTime.ToBinary() >> PrecisionShift);
|
||||
stream.EncodeSignedVariableByteInteger(content.ResponseTime.ToBinary() >> PrecisionShift);
|
||||
}
|
||||
|
||||
public CacheMetadataContent Parse(Stream stream, int length)
|
||||
{
|
||||
CacheMetadataContent content = new CacheMetadataContent();
|
||||
|
||||
content.MaxAge = (uint)stream.DecodeUnsignedVariableByteInteger();
|
||||
content.ETag = stream.ReadLengthPrefixedString();
|
||||
content.LastModified = DateTime.FromBinary(stream.DecodeSignedVariableByteInteger() << PrecisionShift);
|
||||
content.Expires = DateTime.FromBinary(stream.DecodeSignedVariableByteInteger() << PrecisionShift);
|
||||
content.Age = (uint)stream.DecodeUnsignedVariableByteInteger();
|
||||
content.Date = DateTime.FromBinary(stream.DecodeSignedVariableByteInteger() << PrecisionShift);
|
||||
content.Flags = (CacheFlags)stream.DecodeUnsignedVariableByteInteger();
|
||||
content.StaleWhileRevalidate = (uint)stream.DecodeUnsignedVariableByteInteger();
|
||||
content.StaleIfError = (uint)stream.DecodeUnsignedVariableByteInteger();
|
||||
content.RequestTime = DateTime.FromBinary(stream.DecodeSignedVariableByteInteger() << PrecisionShift);
|
||||
content.ResponseTime = DateTime.FromBinary(stream.DecodeSignedVariableByteInteger() << PrecisionShift);
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CacheMetadataIndexingService : IndexingService<CacheMetadataContent, CacheMetadata>
|
||||
{
|
||||
private AVLTree<UnityEngine.Hash128, int> index_Hash = new AVLTree<UnityEngine.Hash128, int>(new Hash128Comparer());
|
||||
|
||||
public override void Index(CacheMetadata metadata)
|
||||
{
|
||||
base.Index(metadata);
|
||||
this.index_Hash.Add(metadata.Hash, metadata.Index);
|
||||
}
|
||||
|
||||
public override void Remove(CacheMetadata metadata)
|
||||
{
|
||||
base.Remove(metadata);
|
||||
this.index_Hash.Remove(metadata.Hash);
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
this.index_Hash.Clear();
|
||||
}
|
||||
|
||||
public override IEnumerable<int> GetOptimizedIndexes() => this.index_Hash.WalkHorizontal();
|
||||
|
||||
public bool ContainsHash(UnityEngine.Hash128 hash) => this.index_Hash.ContainsKey(hash);
|
||||
|
||||
public List<int> FindByHash(UnityEngine.Hash128 hash) => this.index_Hash.Find(hash);
|
||||
}
|
||||
|
||||
internal sealed class CacheMetadataService : MetadataService<CacheMetadata, CacheMetadataContent>
|
||||
{
|
||||
public CacheMetadataService(IndexingService<CacheMetadataContent, CacheMetadata> indexingService, IEmptyMetadataIndexFinder<CacheMetadata> emptyMetadataIndexFinder)
|
||||
: base(indexingService, emptyMetadataIndexFinder)
|
||||
{
|
||||
}
|
||||
|
||||
public override CacheMetadata CreateFrom(Stream stream)
|
||||
{
|
||||
return base.CreateFrom(stream);
|
||||
}
|
||||
|
||||
public CacheMetadata Create(UnityEngine.Hash128 hash, CacheMetadataContent value, int filePos, int length)
|
||||
{
|
||||
var result = base.CreateDefault(value, filePos, length, (content, metadata) => {
|
||||
metadata.Hash = hash;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CacheDatabaseOptions : DatabaseOptions
|
||||
{
|
||||
public CacheDatabaseOptions() : base("CacheDatabase")
|
||||
{
|
||||
base.UseHashFile = false;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class HTTPCacheDatabase : Database<CacheMetadataContent, CacheMetadata, CacheMetadataIndexingService, CacheMetadataService>
|
||||
{
|
||||
public HTTPCacheDatabase(string directory)
|
||||
: this(directory, new CacheDatabaseOptions(), new CacheMetadataIndexingService())
|
||||
{
|
||||
}
|
||||
|
||||
private HTTPCacheDatabase(string directory,
|
||||
DatabaseOptions options,
|
||||
CacheMetadataIndexingService indexingService)
|
||||
: base(directory, options, indexingService, new CacheMetadataContentParser(), new CacheMetadataService(indexingService, new FindDeletedMetadataIndexFinder<CacheMetadata>()))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public CacheMetadataContent FindByHashAndUpdateRequestTime(UnityEngine.Hash128 hash, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(FindByHashAndUpdateRequestTime)}({hash})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return default;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
var (content, metadata) = FindContentAndMetadata(hash);
|
||||
|
||||
if (content != null)
|
||||
{
|
||||
content.RequestTime = DateTime.UtcNow;
|
||||
|
||||
UpdateMetadataAndContent(metadata, content);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
public bool TryAcquireWriteLock(Hash128 hash, Dictionary<string, List<string>> headers, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(TryAcquireWriteLock)}({hash}, {headers?.Count})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return false;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
// FindMetadata filters out logically deleted entries, what we need here because we want to load it too.
|
||||
var metadata = FindMetadata(hash);
|
||||
CacheMetadataContent content = null;
|
||||
if (metadata != null)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(TryAcquireWriteLock)} - Metadata found: {metadata}", context);
|
||||
|
||||
if (metadata.Lock != LockTypes.Unlocked)
|
||||
return false;
|
||||
metadata.Lock = LockTypes.Write;
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
content = this.FromMetadata(metadata);
|
||||
content.From(headers);
|
||||
|
||||
UpdateMetadataAndContent(metadata, content);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(TryAcquireWriteLock)} - Creating new DB entry", context);
|
||||
|
||||
content = new CacheMetadataContent();
|
||||
content.RequestTime = DateTime.UtcNow;
|
||||
content.From(headers);
|
||||
|
||||
(int filePos, int length) = this.DiskManager.Append(content);
|
||||
metadata = this.MetadataService.Create(hash, content, filePos, length);
|
||||
|
||||
metadata.Lock = LockTypes.Write;
|
||||
}
|
||||
|
||||
FlagDirty(1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Update(Hash128 hash, Dictionary<string, List<string>> headers, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(Update)}({hash}, {headers?.Count})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return false;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
var (content, metadata) = FindContentAndMetadata(hash);
|
||||
|
||||
if (content != null)
|
||||
{
|
||||
content.From(headers);
|
||||
content.ResponseTime = DateTime.UtcNow;
|
||||
|
||||
UpdateMetadataAndContent(metadata, content);
|
||||
}
|
||||
|
||||
return content != null;
|
||||
}
|
||||
|
||||
public void ReleaseWriteLock(Hash128 hash, ulong length, bool onlyLockRelease, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(ReleaseWriteLock)}({hash}, {length:N0}, {onlyLockRelease})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
var (content, metadata) = FindContentAndMetadata(hash);
|
||||
if (content == null)
|
||||
{
|
||||
HTTPManager.Logger.Warning(nameof(HTTPCacheDatabase), $"{nameof(ReleaseWriteLock)} - Couldn't find content!", context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata.Lock != LockTypes.Write)
|
||||
HTTPManager.Logger.Error(nameof(HTTPCacheDatabase), $"{nameof(ReleaseWriteLock)} - Is NOT Write Locked! {metadata}", context);
|
||||
|
||||
metadata.Lock = LockTypes.Unlocked;
|
||||
|
||||
if (content != null)
|
||||
{
|
||||
if (!onlyLockRelease)
|
||||
{
|
||||
metadata.ContentLength = length;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
metadata.LastAccessTime = now;
|
||||
content.ResponseTime = now;
|
||||
}
|
||||
|
||||
UpdateMetadataAndContent(metadata, content);
|
||||
}
|
||||
|
||||
FlagDirty(1);
|
||||
}
|
||||
|
||||
public bool TryAcquireReadLock(Hash128 hash, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(TryAcquireReadLock)}({hash})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return false;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
var metadata = FindMetadata(hash);
|
||||
if (metadata == null)
|
||||
return false;
|
||||
|
||||
if (metadata.Lock == LockTypes.Write)
|
||||
return false;
|
||||
|
||||
metadata.Lock = LockTypes.Read;
|
||||
// we are behind a write lock, it's safe to increment it like this
|
||||
metadata.ReadLockCount++;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(TryAcquireReadLock)} - {metadata}", context);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ReleaseReadLock(Hash128 hash, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(ReleaseReadLock)}({hash})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
var metadata = FindMetadata(hash);
|
||||
|
||||
if (metadata == null)
|
||||
{
|
||||
HTTPManager.Logger.Warning(nameof(HTTPCacheDatabase), $"{nameof(ReleaseReadLock)} - Couldn't find metadata!", context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata.Lock != LockTypes.Read)
|
||||
HTTPManager.Logger.Warning(nameof(HTTPCacheDatabase), $"{nameof(ReleaseReadLock)} - Is NOT Locked!", context);
|
||||
|
||||
if (metadata.ReadLockCount == 0)
|
||||
HTTPManager.Logger.Error(nameof(HTTPCacheDatabase), $"{nameof(ReleaseReadLock)} - ReadLockCount already zero!", context);
|
||||
|
||||
if (--metadata.ReadLockCount == 0)
|
||||
metadata.Lock = LockTypes.Unlocked;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(ReleaseReadLock)} - {metadata}", context);
|
||||
}
|
||||
|
||||
internal ulong Delete(Hash128 hash, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(Delete)}({hash})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return 0;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
// Don't use FindMetadata, because it would return null for a logically deleted metadata
|
||||
// so DeleteMetadata wouldn't be called!
|
||||
var byHash = this.IndexingService.FindByHash(hash);
|
||||
if (byHash == null || byHash.Count == 0)
|
||||
return 0;
|
||||
|
||||
var metadata = this.MetadataService.Metadatas[byHash[0]];
|
||||
|
||||
if (metadata == null)
|
||||
return 0;
|
||||
|
||||
var contentLength = metadata.ContentLength;
|
||||
|
||||
base.DeleteMetadata(metadata);
|
||||
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
public void EnterWriteLock(LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(EnterWriteLock)}()", context);
|
||||
this.rwlock.EnterWriteLock();
|
||||
}
|
||||
|
||||
public void ExitWriteLock(LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(ExitWriteLock)}()", context);
|
||||
this.rwlock.ExitWriteLock();
|
||||
}
|
||||
|
||||
public void UpdateLastAccessTime(Hash128 hash, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(UpdateLastAccessTime)}({hash})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
var metadata = FindMetadata(hash);
|
||||
if (metadata != null)
|
||||
{
|
||||
metadata.LastAccessTime = DateTime.UtcNow;
|
||||
FlagDirty(1);
|
||||
}
|
||||
}
|
||||
|
||||
private CacheMetadata FindMetadata(Hash128 hash)
|
||||
{
|
||||
var byHash = this.IndexingService.FindByHash(hash);
|
||||
if (byHash == null || byHash.Count == 0)
|
||||
return null;
|
||||
|
||||
var metadata = this.MetadataService.Metadatas[byHash[0]];
|
||||
|
||||
if (metadata != null && metadata.IsDeleted)
|
||||
return null;
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public (CacheMetadataContent, CacheMetadata) FindContentAndMetadataLocked(Hash128 hash)
|
||||
{
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
return FindContentAndMetadata(hash);
|
||||
}
|
||||
|
||||
private (CacheMetadataContent, CacheMetadata) FindContentAndMetadata(Hash128 hash)
|
||||
{
|
||||
var byHash = this.IndexingService.FindByHash(hash);
|
||||
if (byHash == null || byHash.Count == 0)
|
||||
return (null, null);
|
||||
|
||||
var metadata = this.MetadataService.Metadatas[byHash[0]];
|
||||
if (metadata != null && metadata.IsDeleted)
|
||||
return (null, null);
|
||||
|
||||
var content = this.FromMetadataIndex(metadata.Index);
|
||||
return (content, metadata);
|
||||
}
|
||||
|
||||
private void UpdateMetadataAndContent(Metadata metadata, CacheMetadataContent content)
|
||||
{
|
||||
this.DiskManager.SaveChanged(metadata, content);
|
||||
|
||||
FlagDirty(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7dfaadcb592425e458ee6e074d4135c4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
|
||||
namespace Best.HTTP.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the configuration options for the HTTP cache.
|
||||
/// </summary>
|
||||
public sealed class HTTPCacheOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum duration for which cached entries will be retained.
|
||||
/// </summary>
|
||||
public TimeSpan DeleteOlder { get; internal set; } = TimeSpan.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum size, in bytes, that the cache can reach.
|
||||
/// </summary>
|
||||
public ulong MaxCacheSize { get; internal set; } = 512 * 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HTTPCacheOptions"/> class with default settings.
|
||||
/// </summary>
|
||||
public HTTPCacheOptions()
|
||||
{
|
||||
// Default constructor with no arguments.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HTTPCacheOptions"/> class with custom settings.
|
||||
/// </summary>
|
||||
/// <param name="deleteOlder">The maximum age for cached entries to be retained.</param>
|
||||
/// <param name="maxCacheSize">The maximum size, in bytes, that the cache can reach.</param>
|
||||
public HTTPCacheOptions(TimeSpan deleteOlder, ulong maxCacheSize)
|
||||
{
|
||||
this.DeleteOlder = deleteOlder;
|
||||
this.MaxCacheSize = maxCacheSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d178cd9aca1ad2e45bbf4112596025e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user