-
-
Notifications
You must be signed in to change notification settings - Fork 808
Adds scheduling support to Mocha #9467
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
63be4cd
Adds message scheduling
PascalSenn f2ee805
cleanup
PascalSenn 08465b9
cleanup
PascalSenn d8d3a9c
Refactor scheduled message handling and improve transaction management
PascalSenn 1389dfa
Address PR review comments
PascalSenn f47ed3f
remove in memory native scheduling
PascalSenn 5c8a514
adjust demo
PascalSenn bf315ea
cleanup
PascalSenn 44d0a20
Merge branch 'main' into pse/adds-messaging-scheduler
PascalSenn 7d4a46a
cleanup
PascalSenn f629551
cleanup
PascalSenn 83be00b
cleanup
PascalSenn 3555b8b
fix tests
PascalSenn c086cfe
Merge branch 'main' into pse/adds-messaging-scheduler
PascalSenn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
src/Mocha/src/Mocha.EntityFrameworkCore.Postgres/ScheduledMessageTableInfo.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| namespace Mocha.EntityFrameworkCore.Postgres; | ||
|
|
||
| /// <summary> | ||
| /// Table and column information for the scheduled messages table. | ||
| /// </summary> | ||
| public sealed class ScheduledMessageTableInfo | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets the database schema for the scheduled messages table. Defaults to <c>"public"</c>. | ||
| /// </summary> | ||
| public string Schema { get; set; } = "public"; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the table name for scheduled messages. Defaults to <c>"scheduled_messages"</c>. | ||
| /// </summary> | ||
| public string Table { get; set; } = "scheduled_messages"; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the column name for the scheduled message identifier. Defaults to <c>"id"</c>. | ||
| /// </summary> | ||
| public string Id { get; set; } = "id"; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the column name for the serialized message envelope. Defaults to <c>"envelope"</c>. | ||
| /// </summary> | ||
| public string Envelope { get; set; } = "envelope"; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the column name for the scheduled delivery time. Defaults to <c>"scheduled_time"</c>. | ||
| /// </summary> | ||
| public string ScheduledTime { get; set; } = "scheduled_time"; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the column name tracking how many times dispatch has been attempted. Defaults | ||
| /// to <c>"times_sent"</c>. | ||
| /// </summary> | ||
| public string TimesSent { get; set; } = "times_sent"; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the column name for the message creation timestamp. Defaults to <c>"created_at"</c>. | ||
| /// </summary> | ||
| public string CreatedAt { get; set; } = "created_at"; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the column name for the maximum number of dispatch attempts. Defaults to <c>"max_attempts"</c>. | ||
| /// </summary> | ||
| public string MaxAttempts { get; set; } = "max_attempts"; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the column name for the last error encountered during dispatch. Defaults to <c>"last_error"</c>. | ||
| /// </summary> | ||
| public string LastError { get; set; } = "last_error"; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the name of the primary key index. Defaults to <c>"ix_scheduled_messages_primary_key"</c>. | ||
| /// </summary> | ||
| public string IxPrimaryKey { get; set; } = "ix_scheduled_messages_primary_key"; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the name of the scheduled-time index used for dispatch ordering. Defaults to | ||
| /// <c>"ix_scheduled_messages_scheduled_time"</c>. | ||
| /// </summary> | ||
| public string IxScheduledTime { get; set; } = "ix_scheduled_messages_scheduled_time"; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the name of the times-sent index used for retry filtering. Defaults to | ||
| /// <c>"ix_scheduled_messages_times_sent"</c>. | ||
| /// </summary> | ||
| public string IxTimesSent { get; set; } = "ix_scheduled_messages_times_sent"; | ||
|
|
||
| /// <summary> | ||
| /// Gets the fully qualified table name including schema if not public. | ||
| /// </summary> | ||
| public string QualifiedTableName | ||
| => string.IsNullOrEmpty(Schema) || Schema == "public" ? $"\"{Table}\"" : $"\"{Schema}\".\"{Table}\""; | ||
| } |
137 changes: 137 additions & 0 deletions
137
src/Mocha/src/Mocha.EntityFrameworkCore.Postgres/Scheduling/EfCoreScheduledMessageStore.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| using System.Text.Json; | ||
| using Microsoft.EntityFrameworkCore; | ||
| using Microsoft.EntityFrameworkCore.Storage; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Options; | ||
| using Mocha.Middlewares; | ||
| using Mocha.Utils; | ||
| using Npgsql; | ||
| using NpgsqlTypes; | ||
|
|
||
| namespace Mocha.Scheduling; | ||
|
|
||
| /// <summary> | ||
| /// Implements <see cref="IScheduledMessageStore"/> for Postgres by inserting serialized message envelopes | ||
| /// into the scheduled messages table using raw SQL through the DbContext Npgsql connection. | ||
| /// </summary> | ||
| internal sealed class EfCoreScheduledMessageStore : IScheduledMessageStore, IDisposable | ||
| { | ||
| private readonly DbContext _originalDbContext; | ||
| private readonly ISchedulerSignal _signal; | ||
| private readonly SemaphoreSlim _semaphore = new(1, 1); | ||
| private readonly string? _insertSql; | ||
| private PooledArrayWriter? _arrayWriter; | ||
|
|
||
| /// <summary> | ||
| /// Creates a new <see cref="EfCoreScheduledMessageStore"/> using the provided DbContext connection, | ||
| /// scheduler signal, and pre-built insert SQL. | ||
| /// </summary> | ||
| /// <param name="originalDbContext">The DbContext whose underlying Npgsql connection is used for inserts.</param> | ||
| /// <param name="signal">The signal used to wake the scheduler after a message is persisted.</param> | ||
| /// <param name="insertSql">The parameterized SQL insert statement for the scheduled messages table.</param> | ||
| public EfCoreScheduledMessageStore(DbContext originalDbContext, ISchedulerSignal signal, string insertSql) | ||
| { | ||
| _originalDbContext = originalDbContext; | ||
| _signal = signal; | ||
| _insertSql = insertSql; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Serializes the message envelope and inserts it into the Postgres scheduled messages table. | ||
| /// </summary> | ||
| /// <param name="envelope">The message envelope to persist.</param> | ||
| /// <param name="scheduledTime">The time at which the message should be dispatched.</param> | ||
| /// <param name="cancellationToken">A token to observe for cancellation.</param> | ||
| public async ValueTask PersistAsync( | ||
| MessageEnvelope envelope, | ||
| DateTimeOffset scheduledTime, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| await _semaphore.WaitAsync(cancellationToken); | ||
|
|
||
| try | ||
| { | ||
| _arrayWriter ??= new PooledArrayWriter(); | ||
|
|
||
| var connection = (NpgsqlConnection)_originalDbContext.Database.GetDbConnection(); | ||
|
|
||
| if (connection.State != System.Data.ConnectionState.Open) | ||
| { | ||
| await connection.OpenAsync(cancellationToken); | ||
| } | ||
|
|
||
| var transaction = _originalDbContext.Database.CurrentTransaction?.GetDbTransaction() as NpgsqlTransaction; | ||
|
|
||
| await using var writer = new Utf8JsonWriter(_arrayWriter); | ||
| writer.WriteEnvelope(envelope); | ||
| writer.Flush(); // we know it's not async | ||
|
|
||
| // Execute the INSERT command | ||
| await using var command = connection.CreateCommand(); | ||
| command.CommandText = _insertSql; | ||
| command.Parameters.AddWithValue("@id", NewVersion()); | ||
| command.Parameters.Add( | ||
| new NpgsqlParameter("@envelope", NpgsqlDbType.Json) { Value = _arrayWriter.WrittenMemory }); | ||
| command.Parameters.AddWithValue("@scheduled_time", scheduledTime.UtcDateTime); | ||
| await command.PrepareAsync(cancellationToken); | ||
|
|
||
| await command.ExecuteNonQueryAsync(cancellationToken); | ||
|
|
||
| if (transaction is null) | ||
| { | ||
| _signal.Notify(scheduledTime); | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| _arrayWriter?.Reset(); | ||
| _semaphore.Release(); | ||
| } | ||
| } | ||
|
|
||
| private static Guid NewVersion() | ||
| { | ||
| #if NET9_0_OR_GREATER | ||
| return Guid.CreateVersion7(); | ||
| #else | ||
| return Guid.NewGuid(); | ||
| #endif | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Releases the semaphore and pooled array writer used for scheduled message serialization. | ||
| /// </summary> | ||
| public void Dispose() | ||
| { | ||
| _semaphore.Dispose(); | ||
| _arrayWriter?.Dispose(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates a new <see cref="EfCoreScheduledMessageStore"/> by resolving the DbContext, scheduler signal, | ||
| /// and named options from the scoped service provider. | ||
| /// </summary> | ||
| /// <param name="contextType">The <see cref="Type"/> of the DbContext to resolve.</param> | ||
| /// <param name="optionsName">The named options key used to retrieve <see cref="PostgresScheduledMessageOptions"/>.</param> | ||
| /// <param name="services">The scoped service provider used to resolve dependencies.</param> | ||
| /// <returns>A new <see cref="EfCoreScheduledMessageStore"/> configured for the specified DbContext.</returns> | ||
| public static EfCoreScheduledMessageStore Create(Type contextType, string optionsName, IServiceProvider services) | ||
| { | ||
| var dbContext = (DbContext)services.GetRequiredService(contextType); | ||
| var signal = services.GetRequiredService<ISchedulerSignal>(); | ||
| var optionsMonitor = services.GetRequiredService<IOptionsMonitor<PostgresScheduledMessageOptions>>(); | ||
| var options = optionsMonitor.Get(optionsName); | ||
| var insertSql = options.Queries.InsertMessage; | ||
|
|
||
| return new EfCoreScheduledMessageStore(dbContext, signal, insertSql); | ||
| } | ||
| } | ||
|
|
||
| file static class Extensions | ||
| { | ||
| public static void WriteEnvelope(this Utf8JsonWriter writer, MessageEnvelope envelope) | ||
| { | ||
| var envelopeWriter = new MessageEnvelopeWriter(writer); | ||
| envelopeWriter.WriteMessage(envelope); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.