Initial push

This commit is contained in:
2025-07-20 03:41:39 -04:00
commit d315f5d26e
118 changed files with 25819 additions and 0 deletions

View File

@@ -0,0 +1,199 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using SqrtSpace.SpaceTime.Core;
namespace SqrtSpace.SpaceTime.AspNetCore;
/// <summary>
/// Middleware that enables checkpointing for long-running requests
/// </summary>
public class CheckpointMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<CheckpointMiddleware> _logger;
private readonly CheckpointOptions _options;
public CheckpointMiddleware(
RequestDelegate next,
ILogger<CheckpointMiddleware> logger,
CheckpointOptions options)
{
_next = next;
_logger = logger;
_options = options;
}
public async Task InvokeAsync(HttpContext context)
{
if (!ShouldCheckpoint(context))
{
await _next(context);
return;
}
var checkpointId = context.Request.Headers["X-Checkpoint-Id"].FirstOrDefault();
var checkpointManager = new CheckpointManager(
_options.CheckpointDirectory,
_options.Strategy,
_options.EstimatedOperations);
// Store in HttpContext for access by controllers
context.Features.Set<ICheckpointFeature>(new CheckpointFeature(checkpointManager, checkpointId, _options));
try
{
// If resuming from checkpoint, restore state
if (!string.IsNullOrEmpty(checkpointId))
{
_logger.LogInformation("Resuming from checkpoint {CheckpointId}", checkpointId);
var state = await checkpointManager.RestoreLatestCheckpointAsync<Dictionary<string, object>>();
if (state != null)
{
context.Items["CheckpointState"] = state;
}
}
await _next(context);
}
finally
{
checkpointManager.Dispose();
}
}
private bool ShouldCheckpoint(HttpContext context)
{
// Check if the path matches checkpoint patterns
foreach (var pattern in _options.PathPatterns)
{
if (context.Request.Path.StartsWithSegments(pattern))
{
return true;
}
}
// Check if endpoint has checkpoint attribute
var endpoint = context.GetEndpoint();
if (endpoint != null)
{
var checkpointAttribute = endpoint.Metadata.GetMetadata<EnableCheckpointAttribute>();
return checkpointAttribute != null;
}
return false;
}
}
/// <summary>
/// Options for checkpoint middleware
/// </summary>
public class CheckpointOptions
{
/// <summary>
/// Directory to store checkpoints
/// </summary>
public string? CheckpointDirectory { get; set; }
/// <summary>
/// Checkpointing strategy
/// </summary>
public CheckpointStrategy Strategy { get; set; } = CheckpointStrategy.SqrtN;
/// <summary>
/// Estimated number of operations for √n calculation
/// </summary>
public long EstimatedOperations { get; set; } = 100_000;
/// <summary>
/// Path patterns that should enable checkpointing
/// </summary>
public List<string> PathPatterns { get; set; } = new()
{
"/api/import",
"/api/export",
"/api/process"
};
}
/// <summary>
/// Feature interface for checkpoint access
/// </summary>
public interface ICheckpointFeature
{
CheckpointManager CheckpointManager { get; }
string? CheckpointId { get; }
Task<T?> LoadStateAsync<T>(string key, CancellationToken cancellationToken = default) where T : class;
Task SaveStateAsync<T>(string key, T state, CancellationToken cancellationToken = default) where T : class;
bool ShouldCheckpoint(long currentOperation);
}
/// <summary>
/// Implementation of checkpoint feature
/// </summary>
internal class CheckpointFeature : ICheckpointFeature
{
private readonly CheckpointOptions _options;
private long _operationCount = 0;
public CheckpointFeature(CheckpointManager checkpointManager, string? checkpointId, CheckpointOptions options)
{
CheckpointManager = checkpointManager;
CheckpointId = checkpointId;
_options = options;
}
public CheckpointManager CheckpointManager { get; }
public string? CheckpointId { get; }
public async Task<T?> LoadStateAsync<T>(string key, CancellationToken cancellationToken = default) where T : class
{
if (string.IsNullOrEmpty(CheckpointId))
return null;
return await CheckpointManager.LoadStateAsync<T>(CheckpointId, key, cancellationToken);
}
public async Task SaveStateAsync<T>(string key, T state, CancellationToken cancellationToken = default) where T : class
{
if (string.IsNullOrEmpty(CheckpointId))
return;
await CheckpointManager.SaveStateAsync(CheckpointId, key, state, cancellationToken);
}
public bool ShouldCheckpoint(long currentOperation)
{
_operationCount = currentOperation;
return _options.Strategy switch
{
CheckpointStrategy.SqrtN => currentOperation > 0 && currentOperation % (int)Math.Sqrt(_options.EstimatedOperations) == 0,
CheckpointStrategy.Linear => currentOperation > 0 && currentOperation % 1000 == 0,
CheckpointStrategy.Logarithmic => IsPowerOfTwo(currentOperation),
CheckpointStrategy.None => false,
_ => false
};
}
private static bool IsPowerOfTwo(long n)
{
return n > 0 && (n & (n - 1)) == 0;
}
}
/// <summary>
/// Attribute to enable checkpointing on specific endpoints
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class EnableCheckpointAttribute : Attribute
{
/// <summary>
/// Checkpoint strategy to use
/// </summary>
public CheckpointStrategy Strategy { get; set; } = CheckpointStrategy.SqrtN;
/// <summary>
/// Whether to automatically restore from checkpoint
/// </summary>
public bool AutoRestore { get; set; } = true;
}

View File

@@ -0,0 +1,203 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using SqrtSpace.SpaceTime.Core;
using SqrtSpace.SpaceTime.Diagnostics;
namespace SqrtSpace.SpaceTime.AspNetCore;
/// <summary>
/// Extension methods for configuring SpaceTime services
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds SpaceTime services to the service collection
/// </summary>
public static IServiceCollection AddSpaceTime(
this IServiceCollection services,
Action<SpaceTimeServiceOptions>? configureOptions = null)
{
var options = new SpaceTimeServiceOptions();
configureOptions?.Invoke(options);
// Register options
services.AddSingleton(options);
// Add checkpoint services if enabled
if (options.EnableCheckpointing)
{
services.AddSingleton(options.CheckpointOptions);
}
// Add streaming services if enabled
if (options.EnableStreaming)
{
services.AddSingleton(options.StreamingOptions);
}
return services;
}
/// <summary>
/// Adds SpaceTime middleware to the pipeline
/// </summary>
public static IApplicationBuilder UseSpaceTime(this IApplicationBuilder app)
{
var options = app.ApplicationServices.GetService<SpaceTimeServiceOptions>();
if (options == null)
{
throw new InvalidOperationException("SpaceTime services not registered. Call AddSpaceTime() in ConfigureServices.");
}
if (options.EnableCheckpointing)
{
var checkpointOptions = app.ApplicationServices.GetRequiredService<CheckpointOptions>();
app.UseMiddleware<CheckpointMiddleware>(checkpointOptions);
}
if (options.EnableStreaming)
{
var streamingOptions = app.ApplicationServices.GetRequiredService<ResponseStreamingOptions>();
app.UseMiddleware<ResponseStreamingMiddleware>(streamingOptions);
}
return app;
}
/// <summary>
/// Maps SpaceTime diagnostic and monitoring endpoints
/// </summary>
public static IApplicationBuilder UseSpaceTimeEndpoints(this IApplicationBuilder app)
{
app.UseEndpoints(endpoints =>
{
// Health check endpoint
endpoints.MapGet("/spacetime/health", async context =>
{
context.Response.StatusCode = 200;
await context.Response.WriteAsync("OK");
});
// Metrics endpoint (for Prometheus scraping)
endpoints.MapGet("/spacetime/metrics", async context =>
{
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync("# SpaceTime metrics endpoint\n");
await context.Response.WriteAsync("# Configure OpenTelemetry with Prometheus exporter for metrics\n");
});
// Diagnostics report endpoint
endpoints.MapGet("/spacetime/diagnostics", async context =>
{
var diagnostics = context.RequestServices.GetService<ISpaceTimeDiagnostics>();
if (diagnostics != null)
{
var report = await diagnostics.GenerateReportAsync(TimeSpan.FromHours(1));
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(report);
}
else
{
context.Response.StatusCode = 404;
await context.Response.WriteAsync("Diagnostics not configured");
}
});
// Configuration endpoint
endpoints.MapGet("/spacetime/config", async context =>
{
var options = context.RequestServices.GetService<SpaceTimeServiceOptions>();
if (options != null)
{
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(options);
}
else
{
context.Response.StatusCode = 404;
await context.Response.WriteAsync("Configuration not found");
}
});
});
return app;
}
}
/// <summary>
/// Options for SpaceTime services
/// </summary>
public class SpaceTimeServiceOptions
{
/// <summary>
/// Enable checkpointing middleware
/// </summary>
public bool EnableCheckpointing { get; set; } = true;
/// <summary>
/// Enable streaming optimizations
/// </summary>
public bool EnableStreaming { get; set; } = true;
/// <summary>
/// Options for checkpointing
/// </summary>
public CheckpointOptions CheckpointOptions { get; set; } = new();
/// <summary>
/// Options for streaming
/// </summary>
public ResponseStreamingOptions StreamingOptions { get; set; } = new();
/// <summary>
/// Directory for storing checkpoints
/// </summary>
public string CheckpointDirectory { get; set; } = Path.Combine(Path.GetTempPath(), "spacetime-checkpoints");
/// <summary>
/// Checkpointing strategy to use
/// </summary>
public CheckpointStrategy CheckpointStrategy { get; set; } = CheckpointStrategy.SqrtN;
/// <summary>
/// Interval for checkpointing operations
/// </summary>
public TimeSpan CheckpointInterval { get; set; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Directory for external storage operations
/// </summary>
public string ExternalStorageDirectory { get; set; } = Path.Combine(Path.GetTempPath(), "spacetime-storage");
/// <summary>
/// Default strategy for space-time operations
/// </summary>
public SpaceTimeStrategy DefaultStrategy { get; set; } = SpaceTimeStrategy.SqrtN;
/// <summary>
/// Default chunk size for streaming operations
/// </summary>
public int DefaultChunkSize { get; set; } = 1024;
/// <summary>
/// Buffer size for streaming operations
/// </summary>
public int StreamingBufferSize { get; set; } = 8192;
}
/// <summary>
/// Strategies for space-time tradeoffs
/// </summary>
public enum SpaceTimeStrategy
{
/// <summary>Use √n space strategy</summary>
SqrtN,
/// <summary>Use O(1) space strategy</summary>
Constant,
/// <summary>Use O(log n) space strategy</summary>
Logarithmic,
/// <summary>Use O(n) space strategy</summary>
Linear
}

View File

@@ -0,0 +1,350 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using SqrtSpace.SpaceTime.Core;
namespace SqrtSpace.SpaceTime.AspNetCore;
/// <summary>
/// Extensions for streaming large responses with √n memory usage
/// </summary>
public static class SpaceTimeStreamingExtensions
{
/// <summary>
/// Writes a large enumerable as JSON stream with √n buffering
/// </summary>
public static async Task WriteAsJsonStreamAsync<T>(
this HttpResponse response,
IAsyncEnumerable<T> items,
JsonSerializerOptions? options = null,
CancellationToken cancellationToken = default)
{
response.ContentType = "application/json";
response.Headers.Add("X-SpaceTime-Streaming", "sqrtn");
await using var writer = new Utf8JsonWriter(response.Body, new JsonWriterOptions
{
Indented = options?.WriteIndented ?? false
});
writer.WriteStartArray();
var count = 0;
var bufferSize = SpaceTimeCalculator.CalculateSqrtInterval(100_000); // Estimate
var buffer = new List<T>(bufferSize);
await foreach (var item in items.WithCancellation(cancellationToken))
{
buffer.Add(item);
count++;
if (buffer.Count >= bufferSize)
{
await FlushBufferAsync(writer, buffer, options, cancellationToken);
buffer.Clear();
await response.Body.FlushAsync(cancellationToken);
}
}
// Flush remaining items
if (buffer.Count > 0)
{
await FlushBufferAsync(writer, buffer, options, cancellationToken);
}
writer.WriteEndArray();
await writer.FlushAsync(cancellationToken);
}
/// <summary>
/// Creates an async enumerable result with √n chunking
/// </summary>
public static IActionResult StreamWithSqrtNChunking<T>(
this ControllerBase controller,
IAsyncEnumerable<T> items,
int? estimatedCount = null)
{
return new SpaceTimeStreamResult<T>(items, estimatedCount);
}
private static async Task FlushBufferAsync<T>(
Utf8JsonWriter writer,
List<T> buffer,
JsonSerializerOptions? options,
CancellationToken cancellationToken)
{
foreach (var item in buffer)
{
JsonSerializer.Serialize(writer, item, options);
cancellationToken.ThrowIfCancellationRequested();
}
}
}
/// <summary>
/// Action result for streaming with SpaceTime optimizations
/// </summary>
public class SpaceTimeStreamResult<T> : IActionResult
{
private readonly IAsyncEnumerable<T> _items;
private readonly int? _estimatedCount;
public SpaceTimeStreamResult(IAsyncEnumerable<T> items, int? estimatedCount = null)
{
_items = items;
_estimatedCount = estimatedCount;
}
public async Task ExecuteResultAsync(ActionContext context)
{
var response = context.HttpContext.Response;
response.ContentType = "application/json";
response.Headers.Add("X-SpaceTime-Streaming", "chunked");
if (_estimatedCount.HasValue)
{
response.Headers.Add("X-Total-Count", _estimatedCount.Value.ToString());
}
await response.WriteAsJsonStreamAsync(_items, cancellationToken: context.HttpContext.RequestAborted);
}
}
/// <summary>
/// Attribute to configure streaming behavior
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class SpaceTimeStreamingAttribute : Attribute
{
/// <summary>
/// Chunk size strategy
/// </summary>
public ChunkStrategy ChunkStrategy { get; set; } = ChunkStrategy.SqrtN;
/// <summary>
/// Custom chunk size (if not using automatic strategies)
/// </summary>
public int? ChunkSize { get; set; }
/// <summary>
/// Whether to include progress headers
/// </summary>
public bool IncludeProgress { get; set; } = true;
}
/// <summary>
/// Strategies for determining chunk size
/// </summary>
public enum ChunkStrategy
{
/// <summary>Use √n of estimated total</summary>
SqrtN,
/// <summary>Fixed size chunks</summary>
Fixed,
/// <summary>Adaptive based on response time</summary>
Adaptive
}
/// <summary>
/// Extensions for streaming file downloads
/// </summary>
public static class FileStreamingExtensions
{
/// <summary>
/// Streams a file with √n buffer size
/// </summary>
public static async Task StreamFileWithSqrtNBufferAsync(
this HttpResponse response,
string filePath,
string? contentType = null,
CancellationToken cancellationToken = default)
{
var fileInfo = new FileInfo(filePath);
if (!fileInfo.Exists)
{
response.StatusCode = 404;
return;
}
var bufferSize = (int)SpaceTimeCalculator.CalculateOptimalBufferSize(
fileInfo.Length,
4 * 1024 * 1024); // Max 4MB buffer
response.ContentType = contentType ?? "application/octet-stream";
response.ContentLength = fileInfo.Length;
response.Headers.Add("X-SpaceTime-Buffer-Size", bufferSize.ToString());
await using var fileStream = new FileStream(
filePath,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize,
useAsync: true);
await fileStream.CopyToAsync(response.Body, bufferSize, cancellationToken);
}
}
/// <summary>
/// Middleware for automatic response streaming optimization
/// </summary>
public class ResponseStreamingMiddleware
{
private readonly RequestDelegate _next;
private readonly ResponseStreamingOptions _options;
public ResponseStreamingMiddleware(RequestDelegate next, ResponseStreamingOptions options)
{
_next = next;
_options = options;
}
public async Task InvokeAsync(HttpContext context)
{
// Check if response should be streamed
if (_options.EnableAutoStreaming && IsLargeResponse(context))
{
// Replace response body with buffered stream
var originalBody = context.Response.Body;
using var bufferStream = new SqrtNBufferedStream(originalBody, _options.MaxBufferSize);
context.Response.Body = bufferStream;
try
{
await _next(context);
}
finally
{
context.Response.Body = originalBody;
}
}
else
{
await _next(context);
}
}
private bool IsLargeResponse(HttpContext context)
{
// Check endpoint metadata
var endpoint = context.GetEndpoint();
var streamingAttr = endpoint?.Metadata.GetMetadata<SpaceTimeStreamingAttribute>();
return streamingAttr != null;
}
}
/// <summary>
/// Options for response streaming middleware
/// </summary>
public class ResponseStreamingOptions
{
/// <summary>
/// Enable automatic streaming optimization
/// </summary>
public bool EnableAutoStreaming { get; set; } = true;
/// <summary>
/// Maximum buffer size in bytes
/// </summary>
public int MaxBufferSize { get; set; } = 4 * 1024 * 1024; // 4MB
}
/// <summary>
/// Stream that buffers using √n strategy
/// </summary>
internal class SqrtNBufferedStream : Stream
{
private readonly Stream _innerStream;
private readonly int _bufferSize;
private readonly byte[] _buffer;
private int _bufferPosition;
public SqrtNBufferedStream(Stream innerStream, int maxBufferSize)
{
_innerStream = innerStream;
_bufferSize = Math.Min(maxBufferSize, SpaceTimeCalculator.CalculateSqrtInterval(1_000_000) * 1024);
_buffer = new byte[_bufferSize];
}
public override bool CanRead => _innerStream.CanRead;
public override bool CanSeek => false;
public override bool CanWrite => _innerStream.CanWrite;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override void Flush()
{
if (_bufferPosition > 0)
{
_innerStream.Write(_buffer, 0, _bufferPosition);
_bufferPosition = 0;
}
_innerStream.Flush();
}
public override async Task FlushAsync(CancellationToken cancellationToken)
{
if (_bufferPosition > 0)
{
await _innerStream.WriteAsync(_buffer.AsMemory(0, _bufferPosition), cancellationToken);
_bufferPosition = 0;
}
await _innerStream.FlushAsync(cancellationToken);
}
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
{
while (count > 0)
{
var bytesToCopy = Math.Min(count, _bufferSize - _bufferPosition);
Buffer.BlockCopy(buffer, offset, _buffer, _bufferPosition, bytesToCopy);
_bufferPosition += bytesToCopy;
offset += bytesToCopy;
count -= bytesToCopy;
if (_bufferPosition >= _bufferSize)
{
Flush();
}
}
}
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
var remaining = buffer;
while (remaining.Length > 0)
{
var bytesToCopy = Math.Min(remaining.Length, _bufferSize - _bufferPosition);
remaining.Slice(0, bytesToCopy).CopyTo(_buffer.AsMemory(_bufferPosition));
_bufferPosition += bytesToCopy;
remaining = remaining.Slice(bytesToCopy);
if (_bufferPosition >= _bufferSize)
{
await FlushAsync(cancellationToken);
}
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
Flush();
}
base.Dispose(disposing);
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core middleware and extensions for SpaceTime optimizations</Description>
<PackageId>SqrtSpace.SpaceTime.AspNetCore</PackageId>
<IsPackable>true</IsPackable>
<Authors>David H. Friedel Jr</Authors>
<Company>MarketAlly LLC</Company>
<Copyright>Copyright © 2025 MarketAlly LLC</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/sqrtspace/sqrtspace-dotnet</PackageProjectUrl>
<RepositoryUrl>https://www.sqrtspace.dev</RepositoryUrl>
<RepositoryType>git</RepositoryType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SqrtSpace.SpaceTime.Core\SqrtSpace.SpaceTime.Core.csproj" />
<ProjectReference Include="..\SqrtSpace.SpaceTime.Diagnostics\SqrtSpace.SpaceTime.Diagnostics.csproj" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>