Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
136 commits
Select commit Hold shift + click to select a range
45cf296
Retain completion facts instead of the command in the message enumerator
NinoFloris Aug 28, 2026
8d809b3
Arm per-read cancellation after command dispatch
NinoFloris Aug 28, 2026
bfd5ad7
Reduce backend message publication state
NinoFloris Sep 1, 2026
b96947a
Publish peeked backend messages directly
NinoFloris Sep 1, 2026
330e96a
Use one backend message publication slot
NinoFloris Sep 1, 2026
8dc4f7c
Parse backend messages once before publication
NinoFloris Sep 1, 2026
f285493
Derive backend message offsets on demand
NinoFloris Sep 1, 2026
9b1f51e
Track backend batch sequence positions directly
NinoFloris Sep 1, 2026
b81787c
Publish fallback sequence positions granularly
NinoFloris Sep 1, 2026
1d0038c
Move backend framing into the protocol read pipe
NinoFloris Sep 1, 2026
4adcf26
Rename backend message batches to cursors
NinoFloris Sep 1, 2026
399590e
Retain result-set memory in the protocol read pipe
NinoFloris Sep 1, 2026
7bbe5b1
Keep flow completion arbitration with its owner
NinoFloris Sep 1, 2026
717d131
Decouple result parsing helpers from CommandFlow
NinoFloris Aug 27, 2026
be006e6
Write prepared execution as one reserved span
NinoFloris Aug 28, 2026
6efbe94
Keep shared read state across compatible idle edges
NinoFloris Aug 28, 2026
7b2e8e7
Allow protocol hosts to drive heartbeat ticks
NinoFloris Aug 31, 2026
ea33cc3
Add decoder-owned value row collection
NinoFloris Sep 1, 2026
3f91857
Keep collected row values memory-backed
NinoFloris Sep 2, 2026
5c35d8e
Keep general row memory projection off the array path
NinoFloris Sep 2, 2026
77a0d2b
Peel collected-row loop entry state
NinoFloris Sep 2, 2026
38e1232
Shape collected row advancement for the JIT
NinoFloris Sep 2, 2026
49d63d9
Outline uncommon decoder paths
NinoFloris Sep 2, 2026
d539765
Separate row scanning from exit publication
NinoFloris Sep 2, 2026
e30a6ad
Specialize suspended direct reads
NinoFloris Sep 2, 2026
7f2de0b
Make existing-pipeline admission explicit
NinoFloris Sep 2, 2026
34ac2a2
Skip async result drain after exhaustion
NinoFloris Sep 2, 2026
42b5564
Use a value marker for flow completion
NinoFloris Sep 2, 2026
3a0def4
Signal flow activation with a value marker
NinoFloris Sep 2, 2026
6b02e17
Derive decoder heartbeat ownership from activation
NinoFloris Sep 2, 2026
a966164
Reuse buffered length for stream read sizing
NinoFloris Sep 2, 2026
5c37639
Specialize protocol reads for Runtime Async on net11
NinoFloris Sep 2, 2026
a6c7a73
Outline contiguous projection release
NinoFloris Sep 2, 2026
874411a
Acquire manual-reset completion status
NinoFloris Sep 2, 2026
e936fd5
Skip result callback delivery when unobserved
NinoFloris Sep 2, 2026
140cd98
Avoid reclassifying known protocol messages
NinoFloris Sep 2, 2026
ac1301f
Keep caller cancellation off direct read state
NinoFloris Sep 2, 2026
f3d91e6
Keep backend cursor parsing out of decoder callers
NinoFloris Sep 2, 2026
aa1d534
Keep message peek publication out of decoder callers
NinoFloris Sep 2, 2026
2b5e817
Use array-backed message storage directly
NinoFloris Sep 2, 2026
75f7ce7
Keep parsed message buffers scalar
NinoFloris Sep 2, 2026
2c7dbbe
Let row enumeration classify its publication
NinoFloris Sep 2, 2026
6963b50
Specialize complete prepared message writes
NinoFloris Sep 2, 2026
6cf7bc0
Keep exhausted cursor position scalar
NinoFloris Sep 2, 2026
3eca77d
Specialize two-type terminal validation
NinoFloris Sep 2, 2026
cb950f6
Skip fragmented parsing below one header
NinoFloris Sep 2, 2026
191d0e0
Complete flows from their structural owner
NinoFloris Sep 2, 2026
abdf6e3
Assign reference-bearing values granularly
NinoFloris Sep 2, 2026
0be6ce6
Read transaction status directly from buffered messages
NinoFloris Sep 2, 2026
a8d87d0
Reuse direct decoder read state machine
NinoFloris Sep 2, 2026
27b701a
Reuse streamed message state machines
NinoFloris Sep 3, 2026
bea9508
Bypass policy state machine for settled execution
NinoFloris Sep 3, 2026
66491ed
Fold message projection into its reader continuation
NinoFloris Sep 3, 2026
b2a0529
Fold pipe completion into the decoder continuation
NinoFloris Sep 3, 2026
0ec7189
Read buffered message bodies to their boundary
NinoFloris Sep 3, 2026
0b499f3
Reuse parsed backend message length
NinoFloris Sep 3, 2026
fe42cf9
Reuse backend publication length check
NinoFloris Sep 3, 2026
04758ef
Keep optional flow lifecycle state cold
NinoFloris Sep 3, 2026
d7ec45d
Keep command execution promise across idle reads
NinoFloris Sep 3, 2026
8c66217
Derive deferred command read state from its context
NinoFloris Sep 3, 2026
2a9c559
Dispatch command result publication through the flow
NinoFloris Sep 3, 2026
ed3ac85
Keep command error handling off the read frame
NinoFloris Sep 3, 2026
635a483
Keep generic pipe reads off the direct entry frame
NinoFloris Sep 3, 2026
9a3a939
Move result publication out of the command state machine
NinoFloris Sep 3, 2026
4af2587
Keep timeout draining off the command read frame
NinoFloris Sep 3, 2026
6e92924
Keep resumable writes off the async execution frame
NinoFloris Sep 3, 2026
95cb489
Fuse single prepared command-list writes
NinoFloris Sep 3, 2026
cd5009a
Avoid command copies in prepared write scans
NinoFloris Sep 3, 2026
6ee49f8
Keep preparation reads off the command state machine
NinoFloris Sep 3, 2026
9c9e48f
Keep internal result draining off the reader frame
NinoFloris Sep 3, 2026
8dcdaa6
Reuse buffered length while parsing messages
NinoFloris Sep 3, 2026
813167b
Keep pending stream flushes off the common write frame
NinoFloris Sep 3, 2026
bb6e785
Avoid wait-handle allocation after flow completion
NinoFloris Sep 3, 2026
d9ec971
Keep direct-read completion out of the decoder frame
NinoFloris Sep 3, 2026
1cfce34
Keep cursor buffer publication stack-only
NinoFloris Sep 3, 2026
f0ed421
Cache segment-backed cursor representation
NinoFloris Sep 3, 2026
38a8c51
Keep net11 protocol reads reusable
NinoFloris Sep 3, 2026
3b80f0d
Remove unused flow RFQ callback
NinoFloris Sep 2, 2026
e4f9172
Preserve pooling builders under runtime async
NinoFloris Sep 3, 2026
25cc195
Correct complete-write and reader completion invariants
NinoFloris Sep 3, 2026
d89d9ba
Cover command flow lifecycle and row collection
NinoFloris Sep 4, 2026
00f54f8
Add the command execution flow foundation
NinoFloris Sep 3, 2026
6e4f0cf
Run the command flow suite against the replacement
NinoFloris Sep 3, 2026
7d3e7ae
Classify legacy flow mode switching explicitly
NinoFloris Sep 3, 2026
616cddc
Support preparation and suppressed command results
NinoFloris Sep 3, 2026
a93bee3
Complete replacement flow cancellation and drain semantics
NinoFloris Sep 3, 2026
592b315
Extract the reusable command execution core
NinoFloris Sep 3, 2026
ecf730b
Run ADO commands over the shared execution core
NinoFloris Sep 3, 2026
982cb23
Make the replacement the public command flow
NinoFloris Sep 3, 2026
4bdf524
Detach cancellation bookkeeping from the legacy flow
NinoFloris Sep 3, 2026
54ba4dc
Fast-path single-command execution internally
NinoFloris Sep 4, 2026
763d991
Remove the legacy command flow
NinoFloris Sep 4, 2026
78d4e7b
Restore the scheduler chaos soak helper
NinoFloris Sep 4, 2026
3c7a6f1
Preserve cancellation cadence in the shared command flow
NinoFloris Sep 4, 2026
1dfa1c2
Close command flows activated across shutdown
NinoFloris Sep 4, 2026
33a6600
Keep command consumers off the executor strand
NinoFloris Sep 4, 2026
875915b
Schedule command readiness only off executor activation
NinoFloris Sep 4, 2026
6bd8abc
Add reusable flow authoring contracts
NinoFloris Sep 4, 2026
460d7fc
Cover command flow batching
NinoFloris Sep 4, 2026
9134da2
Expand reusable flow authoring contracts
NinoFloris Sep 4, 2026
a84def1
Restore prepared ADO command fast paths
NinoFloris Sep 4, 2026
2fcfd6f
Restore ADO reader completion fast paths
NinoFloris Sep 4, 2026
a359fc9
Reuse resolved connections during reader creation
NinoFloris Sep 4, 2026
dfd3c9b
Derive ADO flow lifetime ownership from its binding
NinoFloris Sep 4, 2026
edffeb7
Name the shared command flow core directly
NinoFloris Sep 4, 2026
1c73b8a
Replace delegated field references with owner contracts
NinoFloris Sep 4, 2026
a3f8c6d
Update the backend publication benchmark for cursors
NinoFloris Sep 4, 2026
519f4b5
Expose buffered command result collection
NinoFloris Sep 4, 2026
2962a53
Preserve buffered successors after result retention
NinoFloris Sep 4, 2026
10c1840
Trim duplicate command flow async state
NinoFloris Sep 4, 2026
2be3748
Reuse protocol result state across completion awaits
NinoFloris Sep 4, 2026
b6faab7
Split command result read state machines
NinoFloris Sep 4, 2026
c35f529
Avoid generic source driver dispatch wrapper
NinoFloris Sep 4, 2026
9a95cad
Use runtime async for normal pipe operations on net11
NinoFloris Sep 5, 2026
7a4eb36
Upgrade initial publication timestamp proof
NinoFloris Sep 5, 2026
4d88eaf
Advance retained results to their active buffer origin
NinoFloris Sep 5, 2026
a10f0b1
Restore stream reader un-examine state
NinoFloris Sep 5, 2026
82e6137
Release completed read grants at the idle edge
NinoFloris Sep 5, 2026
6215548
Allow the PantherTE comparison harness
NinoFloris Sep 3, 2026
7e0b7d8
Route Slon continuations through the ambient scheduler
NinoFloris Sep 3, 2026
91a6962
Allow unsafe unified flow pooling experiment
NinoFloris Sep 4, 2026
28a4fd5
Prototype tenure-safe pooled command flows
NinoFloris Sep 4, 2026
e0d061d
Retain activation waiter only after flow reuse
NinoFloris Sep 4, 2026
32dbf23
Shrink runtime async pipe read suspension state
NinoFloris Sep 5, 2026
8f7d684
Add Slon fortunes benchmarks
sebastienros Aug 27, 2026
04016c6
Benchmark Slon through its lower protocol layer
NinoFloris Aug 27, 2026
60a7f6f
Compare raw and production pool placement
NinoFloris Aug 28, 2026
16f3095
Benchmark streaming and collected Slon results
NinoFloris Aug 28, 2026
188cace
Render fortunes from retained UTF-8 fields
NinoFloris Aug 31, 2026
21e84cb
Tighten Fortunes consumption paths
NinoFloris Aug 31, 2026
1042048
Drive pooled protocol heartbeats centrally
NinoFloris Aug 31, 2026
374e2bd
Use one production Fortunes path
NinoFloris Aug 31, 2026
7c5cb6a
Add raw Fortunes templating
NinoFloris Aug 31, 2026
a4fc50c
Run Slon fortunes through command result collection
NinoFloris Sep 4, 2026
c65b871
Benchmark reusable command flows in fortunes apps
NinoFloris Sep 5, 2026
1fdcc48
Retain Slon row memory through minimal rendering
NinoFloris Sep 5, 2026
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
2 changes: 1 addition & 1 deletion Slon.Benchmark/AdoCommandFlowFactoryBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,5 @@ public async Task Cleanup()
}

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_batchCore")]
static extern ref AdoBatchCore<SlonBatchCommand> GetBatchCore(SlonBatch batch);
static extern ref AdoBatchCore<SlonBatchCommand, SlonBatch.BatchCoreRef> GetBatchCore(SlonBatch batch);
}
4 changes: 2 additions & 2 deletions Slon.Benchmark/BackendMessagePublicationBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ public class BackendMessagePublicationBenchmark
[Benchmark]
public int PublishBufferedMessages()
{
_context.RetireCurrentBatch();
_context.SetBatch(new BackendMessageBatch(new ReadOnlySequence<byte>(_messages)));
_context.RetireCursor();
_context.SetCursor(new BackendMessageCursor(new ReadOnlySequence<byte>(_messages)));
var count = 0;
while (_context.TryMoveNext())
count++;
Expand Down
270 changes: 270 additions & 0 deletions Slon.Benchmarks/Shared/SlonConnectionPool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
using System.Diagnostics;
using System.Globalization;
using System.Net;
using Microsoft.Extensions.ObjectPool;
using Npgsql;
using Slon.Pg;
using Slon.Pg.Protocol;
using Slon.Pg.Protocol.Flows;
using Slon.Pooling;
using Slon.Text;
using Slon.Transport;

namespace Slon.Fortunes;

internal sealed class SlonConnectionPool : IAsyncDisposable
{
const string Query = "SELECT id, message FROM fortune";
readonly ConnectionPool<ProtocolConnection> _pool;
readonly CommandFlowOptions _options;
readonly ObjectPool<CommandFlow>? _flowPool;

SlonConnectionPool(
ConnectionPool<ProtocolConnection> pool,
Command command,
int flowPoolCapacity)
{
_pool = pool;
_options = new() { Commands = new(command) };
if (flowPoolCapacity > 0)
{
_flowPool = new DefaultObjectPool<CommandFlow>(
new CommandFlowPoolPolicy(), flowPoolCapacity);
}
}

internal static async ValueTask<SlonConnectionPool> CreateAsync(
string connectionString,
int connectionCount)
{
var builder = new NpgsqlConnectionStringBuilder(connectionString);
var clientOptions = new PgClientOptions
{
EndPoint = new DnsEndPoint(
RequiredPostgreSqlValue("Host", builder.Host), builder.Port),
Database = RequiredPostgreSqlValue("Database", builder.Database),
Username = RequiredPostgreSqlValue("Username", builder.Username),
Password = builder.Password,
Ssl = new PostgreSqlSslOptions { Mode = PostgreSqlSslMode.Disable },
};
var transportFactory = SocketStreamConnection.CreateFactory(
clientOptions.EndPoint,
new TransportConnectionOptions { UseZeroByteReads = false });
var bootstrapFactory = new PgClientProtocolFactory(clientOptions, transportFactory);
var protocolFactory = new PgClientProtocolFactory(
clientOptions,
transportFactory,
static options => options.HeartbeatMode = PgClientProtocolHeartbeatMode.External);

// Every pooled wire installs the same named statement. Obtain its immutable descriptor once;
// later flows can be created before placement and use it on whichever wire the pool selects.
Command command;
await using (var bootstrap = await bootstrapFactory.CreateAsync().ConfigureAwait(false))
command = await PrepareAsync(bootstrap).ConfigureAwait(false);

var pool = new ConnectionPool<ProtocolConnection>(
new ProtocolConnectionFactory(protocolFactory),
new ConnectionPoolOptions
{
MinConnections = connectionCount,
MaxConnections = connectionCount,
ConnectionIdleLifetime = Timeout.InfiniteTimeSpan,
});
return new(pool, command, GetFlowPoolCapacity());
}

public async ValueTask ConsumeRetainedAsync<T, TState>(
Func<int, ReadOnlyMemory<byte>, T> create,
TState state,
Func<TState, List<T>, ValueTask> consume,
CancellationToken cancellationToken)
{
var flow = RentFlow();
await _pool.GetAsync(
static (candidate, item) => candidate.Connection.Protocol.TryQueue(
item,
candidate.IsIdleCandidate
? FlowEnqueueOptions.None
: FlowEnqueueOptions.RequireExistingPipeline,
candidate.CancellationToken),
flow,
Timeout.InfiniteTimeSpan,
cancellationToken).ConfigureAwait(false);

var values = new List<T>();
var results = flow.GetAsyncEnumerator(cancellationToken);
try
{
if (await results.MoveNextAsync().ConfigureAwait(false))
{
results.Current.EnableResultBuffering();
await results.Current.CollectAsync(
(Values: values, Create: create),
static (collection, row) => collection.Values.Add(
collection.Create(
row.GetInt32(0), row.BorrowFieldMemory(1))),
cancellationToken).ConfigureAwait(false);
}
await consume(state, values).ConfigureAwait(false);
}
finally
{
await results.DisposeAsync().ConfigureAwait(false);
_flowPool?.Return(flow);
}
}

public ValueTask DisposeAsync() => _pool.DisposeAsync();

CommandFlow RentFlow()
{
var flow = _flowPool?.Get();
return flow is null
? new CommandFlow(async: true, _options)
: flow.Initialize(async: true, _options);
}

static int GetFlowPoolCapacity()
{
const string name = "SLON_FLOW_POOL_CAPACITY";
var value = Environment.GetEnvironmentVariable(name);
if (string.IsNullOrWhiteSpace(value))
return 0;
return int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out var capacity)
&& capacity >= 0
? capacity
: throw new InvalidOperationException($"{name} must be a non-negative integer.");
}

sealed class CommandFlowPoolPolicy : PooledObjectPolicy<CommandFlow>
{
public override CommandFlow Create()
=> new(async: true, ReadOnlySpan<Command>.Empty);

public override bool Return(CommandFlow flow)
{
// DisposeAsync crosses the framework's retirement boundary before a flow reaches here.
// Reset therefore cannot overlap the old tenure's protocol or heartbeat observation.
flow.Reset();
return true;
}
}

static async ValueTask<Command> PrepareAsync(PgClientProtocol protocol)
{
var command = Command.Create(Query, commandName: new EncodedCString("fortunes"));
var flow = protocol.Queue(new CommandFlow(async: true, command));
Command? prepared = null;
await foreach (var result in flow)
{
var metadata = result.GetMetadata();
prepared = Command.Create(CommandDescriptor.CreatePrepared(
metadata.CommandName,
metadata.ParameterTypes.Preserve(),
metadata.RowDescription?.Preserve()));
await foreach (var _ in result) { }
_ = result.GetCommandComplete();
}
return prepared ??
throw new InvalidOperationException("PostgreSQL preparation returned no command result.");
}

static string RequiredPostgreSqlValue(string name, string? value)
=> string.IsNullOrWhiteSpace(value)
? throw new InvalidOperationException($"PostgreSQL {name} is required.")
: value;

sealed class ProtocolConnection(PgClientProtocol protocol)
: IPoolConnection<ProtocolConnection>
{
IDisposable? _heartbeatRegistration;

internal PgClientProtocol Protocol { get; } = protocol;
public bool IsIdle => Protocol.Outstanding == 0;
public bool IsSchedulable => Protocol.IsSchedulable;
public Task Completion => Protocol.Completion;
public Task CompleteAsync(Exception? exception = null)
{
var completion = Protocol.CompleteAsync(exception);
if (completion.IsCompleted)
{
StopHeartbeat();
return completion;
}
return CompleteAndStopHeartbeat(completion, this);

static async Task CompleteAndStopHeartbeat(
Task completion, ProtocolConnection connection)
{
try
{
await completion.ConfigureAwait(false);
}
finally
{
connection.StopHeartbeat();
}
}
}
public int CompareTo(ProtocolConnection? other)
=> other is null ? 1 : Protocol.Outstanding.CompareTo(other.Protocol.Outstanding);

public void Start(ConnectionPool<ProtocolConnection>.Registration registration)
=> Protocol.SetAdmissionAvailableCallback(
() => registration.SignalAvailability(Protocol.Outstanding == 0));

internal void StartHeartbeat(ConnectionPoolContext<ProtocolConnection> poolContext)
{
Debug.Assert(_heartbeatRegistration is null);
_heartbeatRegistration = poolContext.OnHeartbeat(
static (connection, elapsed) => connection.Protocol.HeartbeatAsync(elapsed), this);
}

internal void StopHeartbeat()
=> Interlocked.Exchange(ref _heartbeatRegistration, null)?.Dispose();
}

sealed class ProtocolConnectionFactory(PgClientProtocolFactory factory)
: IPoolConnectionFactory<ProtocolConnection>
{
public ProtocolConnection Create(
ConnectionPoolContext<ProtocolConnection> poolContext,
TimeSpan timeout = default)
{
var protocol = factory.Create(timeout);
var connection = new ProtocolConnection(protocol);
connection.StartHeartbeat(poolContext);
try
{
_ = PrepareAsync(protocol).AsTask().GetAwaiter().GetResult();
return connection;
}
catch
{
connection.StopHeartbeat();
protocol.Dispose();
throw;
}
}

public async ValueTask<ProtocolConnection> CreateAsync(
ConnectionPoolContext<ProtocolConnection> poolContext,
CancellationToken cancellationToken = default)
{
var protocol = await factory.CreateAsync(cancellationToken).ConfigureAwait(false);
var connection = new ProtocolConnection(protocol);
connection.StartHeartbeat(poolContext);
try
{
_ = await PrepareAsync(protocol).ConfigureAwait(false);
return connection;
}
catch
{
connection.StopHeartbeat();
await protocol.DisposeAsync().ConfigureAwait(false);
throw;
}
}
}
}
17 changes: 17 additions & 0 deletions Slon.Benchmarks/Slon.Fortunes.Minimal/Fortune.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Slon.Fortunes.Minimal;

public readonly struct Fortune : IComparable<Fortune>
{
public Fortune(int id, ReadOnlyMemory<byte> message)
{
Id = id;
Message = message;
}

public int Id { get; }

public ReadOnlyMemory<byte> Message { get; }

public int CompareTo(Fortune other) =>
Message.Span.SequenceCompareTo(other.Message.Span);
}
Loading