diff --git a/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/20260804222120_AddRetryHistory.Designer.cs b/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/20260804222120_AddRetryHistory.Designer.cs new file mode 100644 index 0000000000..4bb68b0a7b --- /dev/null +++ b/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/20260804222120_AddRetryHistory.Designer.cs @@ -0,0 +1,717 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using ServiceControl.Persistence.EFCore.PostgreSql; + +#nullable disable + +namespace ServiceControl.Persistence.EFCore.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlServiceControlDbContext))] + [Migration("20260804222120_AddRetryHistory")] + partial class AddRetryHistory + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.CustomCheckEntity", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Category") + .IsRequired() + .HasColumnType("text") + .HasColumnName("category"); + + b.Property("CustomCheckId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("custom_check_id"); + + b.Property("FailureReason") + .HasColumnType("text") + .HasColumnName("failure_reason"); + + b.Property("OriginatingEndpointHost") + .IsRequired() + .HasColumnType("text") + .HasColumnName("originating_endpoint_host"); + + b.Property("OriginatingEndpointHostId") + .HasColumnType("uuid") + .HasColumnName("originating_endpoint_host_id"); + + b.Property("OriginatingEndpointName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("originating_endpoint_name"); + + b.Property("ReportedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("reported_at"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_custom_checks"); + + b.HasIndex("ReportedAt") + .HasDatabaseName("ix_custom_checks_reported_at"); + + b.HasIndex("Status", "ReportedAt") + .HasDatabaseName("ix_custom_checks_status_reported_at"); + + b.ToTable("custom_checks", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.EndpointSettingsEntity", b => + { + b.Property("Name") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("name"); + + b.Property("TrackInstances") + .HasColumnType("boolean") + .HasColumnName("track_instances"); + + b.HasKey("Name") + .HasName("pk_endpoint_settings"); + + b.ToTable("endpoint_settings", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.EventLogItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("category"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("event_type"); + + b.Property("RaisedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("raised_at"); + + b.PrimitiveCollection>("RelatedTo") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("related_to"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("pk_event_log_items"); + + b.HasIndex("RaisedAt", "Id") + .IsDescending() + .HasDatabaseName("ix_event_log_items_raised_at_id"); + + b.ToTable("EventLogItems", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedErrorImportEntity", b => + { + b.Property("UniqueMessageId") + .HasColumnType("uuid") + .HasColumnName("unique_message_id"); + + b.Property("Body") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("body"); + + b.Property("BodyStoredExternally") + .HasColumnType("boolean") + .HasColumnName("body_stored_externally"); + + b.Property("ExceptionInfo") + .IsRequired() + .HasColumnType("text") + .HasColumnName("exception_info"); + + b.Property("FailedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("failed_at"); + + b.Property("HeadersJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("headers_json"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("message_id"); + + b.HasKey("UniqueMessageId") + .HasName("pk_failed_error_imports"); + + b.HasIndex("FailedAt") + .HasDatabaseName("ix_failed_error_imports_failed_at"); + + b.ToTable("failed_error_imports", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageEntity", b => + { + b.Property("UniqueMessageId") + .HasColumnType("uuid") + .HasColumnName("unique_message_id"); + + b.Property("BodyContentType") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("body_content_type"); + + b.Property("BodySize") + .HasColumnType("integer") + .HasColumnName("body_size"); + + b.Property("BodyStoredExternally") + .HasColumnType("boolean") + .HasColumnName("body_stored_externally"); + + b.Property("BodyText") + .HasColumnType("text") + .HasColumnName("body_text"); + + b.Property("ConversationId") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("conversation_id"); + + b.Property("ExceptionMessage") + .HasColumnType("text") + .HasColumnName("exception_message"); + + b.Property("ExceptionType") + .HasColumnType("text") + .HasColumnName("exception_type"); + + b.Property("FailingEndpointAddress") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("failing_endpoint_address"); + + b.Property("FirstTimeOfFailure") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_time_of_failure"); + + b.Property("HeadersJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("headers_json"); + + b.Property("IsSystemMessage") + .HasColumnType("boolean") + .HasColumnName("is_system_message"); + + b.Property("LastAttemptedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempted_at"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.Property("LastTimeOfFailure") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_time_of_failure"); + + b.Property("MessageId") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("message_id"); + + b.Property("MessageType") + .HasColumnType("text") + .HasColumnName("message_type"); + + b.Property("NumberOfProcessingAttempts") + .HasColumnType("integer") + .HasColumnName("number_of_processing_attempts"); + + b.Property("ReceivingEndpointHost") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("receiving_endpoint_host"); + + b.Property("ReceivingEndpointHostId") + .HasColumnType("uuid") + .HasColumnName("receiving_endpoint_host_id"); + + b.Property("ReceivingEndpointName") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("receiving_endpoint_name"); + + b.Property("SendingEndpointHost") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("sending_endpoint_host"); + + b.Property("SendingEndpointHostId") + .HasColumnType("uuid") + .HasColumnName("sending_endpoint_host_id"); + + b.Property("SendingEndpointName") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("sending_endpoint_name"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("StatusChangedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("status_changed_at"); + + b.Property("TimeSent") + .HasColumnType("timestamp with time zone") + .HasColumnName("time_sent"); + + b.HasKey("UniqueMessageId") + .HasName("pk_failed_messages"); + + b.HasIndex("ConversationId") + .HasDatabaseName("ix_failed_messages_conversation_id"); + + b.HasIndex("FailingEndpointAddress") + .HasDatabaseName("ix_failed_messages_failing_endpoint_address"); + + b.HasIndex("ReceivingEndpointName") + .HasDatabaseName("ix_failed_messages_receiving_endpoint_name"); + + b.HasIndex("StatusChangedAt") + .HasDatabaseName("ix_failed_messages_status_changed_at") + .HasFilter("status IN (2, 4)"); + + b.HasIndex("TimeSent") + .HasDatabaseName("ix_failed_messages_time_sent"); + + b.HasIndex("Status", "LastModified") + .HasDatabaseName("ix_failed_messages_status_last_modified"); + + b.ToTable("failed_messages", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageGroupEntity", b => + { + b.Property("FailedMessageUniqueId") + .HasColumnType("uuid") + .HasColumnName("failed_message_unique_id"); + + b.Property("GroupId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("group_id"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("type"); + + b.HasKey("FailedMessageUniqueId", "GroupId") + .HasName("pk_failed_message_groups"); + + b.HasIndex("GroupId") + .HasDatabaseName("ix_failed_message_groups_group_id"); + + b.HasIndex("Type", "GroupId") + .HasDatabaseName("ix_failed_message_groups_type_group_id"); + + b.ToTable("failed_message_groups", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageRetryEntity", b => + { + b.Property("UniqueMessageId") + .HasColumnType("uuid") + .HasColumnName("unique_message_id"); + + b.Property("RetryBatchId") + .HasColumnType("uuid") + .HasColumnName("retry_batch_id"); + + b.Property("StageAttempts") + .HasColumnType("integer") + .HasColumnName("stage_attempts"); + + b.HasKey("UniqueMessageId") + .HasName("pk_failed_message_retries"); + + b.HasIndex("RetryBatchId") + .HasDatabaseName("ix_failed_message_retries_retry_batch_id"); + + b.ToTable("failed_message_retries", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.GroupCommentEntity", b => + { + b.Property("GroupId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("group_id"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text") + .HasColumnName("comment"); + + b.HasKey("GroupId") + .HasName("pk_group_comments"); + + b.ToTable("group_comments", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.HistoricRetryOperationEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CompletionTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("completion_time"); + + b.Property("Failed") + .HasColumnType("boolean") + .HasColumnName("failed"); + + b.Property("NumberOfMessagesProcessed") + .HasColumnType("integer") + .HasColumnName("number_of_messages_processed"); + + b.Property("Originator") + .HasColumnType("text") + .HasColumnName("originator"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("request_id"); + + b.Property("RetryType") + .HasColumnType("integer") + .HasColumnName("retry_type"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time"); + + b.HasKey("Id") + .HasName("pk_historic_retry_operations"); + + b.HasIndex("CompletionTime", "Id") + .IsDescending() + .HasDatabaseName("ix_historic_retry_operations_completion_time_id"); + + b.ToTable("historic_retry_operations", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.KnownEndpointEntity", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Host") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("host"); + + b.Property("HostId") + .HasColumnType("uuid") + .HasColumnName("host_id"); + + b.Property("Monitored") + .HasColumnType("boolean") + .HasColumnName("monitored"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_known_endpoints"); + + b.ToTable("known_endpoints", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.MessageRedirectEntity", b => + { + b.Property("FromPhysicalAddress") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("from_physical_address"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.Property("ToPhysicalAddress") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("to_physical_address"); + + b.HasKey("FromPhysicalAddress") + .HasName("pk_message_redirects"); + + b.ToTable("message_redirects", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.RetryBatchEntity", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Classifier") + .HasColumnType("text") + .HasColumnName("classifier"); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("InitialBatchSize") + .HasColumnType("integer") + .HasColumnName("initial_batch_size"); + + b.Property("InitiatedById") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("initiated_by_id"); + + b.Property("InitiatedByName") + .HasColumnType("text") + .HasColumnName("initiated_by_name"); + + b.Property("Last") + .HasColumnType("timestamp with time zone") + .HasColumnName("last"); + + b.Property("OperationId") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("operation_id"); + + b.Property("Originator") + .HasColumnType("text") + .HasColumnName("originator"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("request_id"); + + b.Property("RetrySessionId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("retry_session_id"); + + b.Property("RetryType") + .HasColumnType("integer") + .HasColumnName("retry_type"); + + b.Property("StagingId") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("staging_id"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_retry_batches"); + + b.HasIndex("Status", "RetrySessionId") + .HasDatabaseName("ix_retry_batches_status_retry_session_id"); + + b.ToTable("retry_batches", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.RetryBatchNowForwardingEntity", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("RetryBatchId") + .HasColumnType("uuid") + .HasColumnName("retry_batch_id"); + + b.HasKey("Id") + .HasName("pk_retry_batch_now_forwarding"); + + b.ToTable("retry_batch_now_forwarding", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.SubscriptionEntity", b => + { + b.Property("MessageType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("message_type"); + + b.Property("TransportAddress") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("transport_address"); + + b.Property("Endpoint") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("endpoint"); + + b.HasKey("MessageType", "TransportAddress") + .HasName("pk_subscriptions"); + + b.ToTable("subscriptions", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.TrialMetadataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("TrialEndDate") + .HasColumnType("date") + .HasColumnName("trial_end_date"); + + b.HasKey("Id") + .HasName("pk_trial_metadata"); + + b.ToTable("trial_metadata", (string)null); + + b.HasData( + new + { + Id = 1 + }); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.UnacknowledgedRetryOperationEntity", b => + { + b.Property("RequestId") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("request_id"); + + b.Property("RetryType") + .HasColumnType("integer") + .HasColumnName("retry_type"); + + b.Property("Classifier") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("classifier"); + + b.Property("CompletionTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("completion_time"); + + b.Property("Failed") + .HasColumnType("boolean") + .HasColumnName("failed"); + + b.Property("Last") + .HasColumnType("timestamp with time zone") + .HasColumnName("last"); + + b.Property("NumberOfMessagesProcessed") + .HasColumnType("integer") + .HasColumnName("number_of_messages_processed"); + + b.Property("Originator") + .HasColumnType("text") + .HasColumnName("originator"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time"); + + b.HasKey("RequestId", "RetryType") + .HasName("pk_unacknowledged_retry_operations"); + + b.ToTable("unacknowledged_retry_operations", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageGroupEntity", b => + { + b.HasOne("ServiceControl.Persistence.EFCore.Entities.FailedMessageEntity", null) + .WithMany() + .HasForeignKey("FailedMessageUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_failed_message_groups_failed_messages_failed_message_unique"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/20260804222120_AddRetryHistory.cs b/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/20260804222120_AddRetryHistory.cs new file mode 100644 index 0000000000..3d2ba5f304 --- /dev/null +++ b/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/20260804222120_AddRetryHistory.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ServiceControl.Persistence.EFCore.PostgreSql.Migrations +{ + /// + public partial class AddRetryHistory : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "historic_retry_operations", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + request_id = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), + retry_type = table.Column(type: "integer", nullable: false), + start_time = table.Column(type: "timestamp with time zone", nullable: false), + completion_time = table.Column(type: "timestamp with time zone", nullable: false), + originator = table.Column(type: "text", nullable: true), + failed = table.Column(type: "boolean", nullable: false), + number_of_messages_processed = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_historic_retry_operations", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "unacknowledged_retry_operations", + columns: table => new + { + request_id = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), + retry_type = table.Column(type: "integer", nullable: false), + start_time = table.Column(type: "timestamp with time zone", nullable: false), + completion_time = table.Column(type: "timestamp with time zone", nullable: false), + last = table.Column(type: "timestamp with time zone", nullable: false), + originator = table.Column(type: "text", nullable: true), + classifier = table.Column(type: "character varying(450)", maxLength: 450, nullable: true), + failed = table.Column(type: "boolean", nullable: false), + number_of_messages_processed = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_unacknowledged_retry_operations", x => new { x.request_id, x.retry_type }); + }); + + migrationBuilder.CreateIndex( + name: "ix_historic_retry_operations_completion_time_id", + table: "historic_retry_operations", + columns: new[] { "completion_time", "id" }, + descending: new bool[0]); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "historic_retry_operations"); + + migrationBuilder.DropTable( + name: "unacknowledged_retry_operations"); + } + } +} diff --git a/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/PostgreSqlServiceControlDbContextModelSnapshot.cs b/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/PostgreSqlServiceControlDbContextModelSnapshot.cs index 8184cac7ba..90551c8188 100644 --- a/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/PostgreSqlServiceControlDbContextModelSnapshot.cs +++ b/src/ServiceControl.Persistence.EFCore.PostgreSql/Migrations/PostgreSqlServiceControlDbContextModelSnapshot.cs @@ -406,6 +406,55 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("group_comments", (string)null); }); + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.HistoricRetryOperationEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CompletionTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("completion_time"); + + b.Property("Failed") + .HasColumnType("boolean") + .HasColumnName("failed"); + + b.Property("NumberOfMessagesProcessed") + .HasColumnType("integer") + .HasColumnName("number_of_messages_processed"); + + b.Property("Originator") + .HasColumnType("text") + .HasColumnName("originator"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("request_id"); + + b.Property("RetryType") + .HasColumnType("integer") + .HasColumnName("retry_type"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time"); + + b.HasKey("Id") + .HasName("pk_historic_retry_operations"); + + b.HasIndex("CompletionTime", "Id") + .IsDescending() + .HasDatabaseName("ix_historic_retry_operations_completion_time_id"); + + b.ToTable("historic_retry_operations", (string)null); + }); + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.KnownEndpointEntity", b => { b.Property("Id") @@ -604,6 +653,52 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.UnacknowledgedRetryOperationEntity", b => + { + b.Property("RequestId") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("request_id"); + + b.Property("RetryType") + .HasColumnType("integer") + .HasColumnName("retry_type"); + + b.Property("Classifier") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("classifier"); + + b.Property("CompletionTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("completion_time"); + + b.Property("Failed") + .HasColumnType("boolean") + .HasColumnName("failed"); + + b.Property("Last") + .HasColumnType("timestamp with time zone") + .HasColumnName("last"); + + b.Property("NumberOfMessagesProcessed") + .HasColumnType("integer") + .HasColumnName("number_of_messages_processed"); + + b.Property("Originator") + .HasColumnType("text") + .HasColumnName("originator"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time"); + + b.HasKey("RequestId", "RetryType") + .HasName("pk_unacknowledged_retry_operations"); + + b.ToTable("unacknowledged_retry_operations", (string)null); + }); + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageGroupEntity", b => { b.HasOne("ServiceControl.Persistence.EFCore.Entities.FailedMessageEntity", null) diff --git a/src/ServiceControl.Persistence.EFCore.SqlServer/Migrations/20260804222033_AddRetryHistory.Designer.cs b/src/ServiceControl.Persistence.EFCore.SqlServer/Migrations/20260804222033_AddRetryHistory.Designer.cs new file mode 100644 index 0000000000..2a21d7a2ed --- /dev/null +++ b/src/ServiceControl.Persistence.EFCore.SqlServer/Migrations/20260804222033_AddRetryHistory.Designer.cs @@ -0,0 +1,576 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ServiceControl.Persistence.EFCore.SqlServer; + +#nullable disable + +namespace ServiceControl.Persistence.EFCore.SqlServer.Migrations +{ + [DbContext(typeof(SqlServerServiceControlDbContext))] + [Migration("20260804222033_AddRetryHistory")] + partial class AddRetryHistory + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.CustomCheckEntity", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CustomCheckId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FailureReason") + .HasColumnType("nvarchar(max)"); + + b.Property("OriginatingEndpointHost") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OriginatingEndpointHostId") + .HasColumnType("uniqueidentifier"); + + b.Property("OriginatingEndpointName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReportedAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ReportedAt"); + + b.HasIndex("Status", "ReportedAt"); + + b.ToTable("CustomChecks"); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.EndpointSettingsEntity", b => + { + b.Property("Name") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("TrackInstances") + .HasColumnType("bit"); + + b.HasKey("Name"); + + b.ToTable("EndpointSettings"); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.EventLogItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("RaisedAt") + .HasColumnType("datetime2"); + + b.PrimitiveCollection("RelatedTo") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Severity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RaisedAt", "Id") + .IsDescending(); + + b.ToTable("EventLogItems", (string)null); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedErrorImportEntity", b => + { + b.Property("UniqueMessageId") + .HasColumnType("uniqueidentifier"); + + b.Property("Body") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("BodyStoredExternally") + .HasColumnType("bit"); + + b.Property("ExceptionInfo") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FailedAt") + .HasColumnType("datetime2"); + + b.Property("HeadersJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.HasKey("UniqueMessageId"); + + b.HasIndex("FailedAt"); + + b.ToTable("FailedErrorImports"); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageEntity", b => + { + b.Property("UniqueMessageId") + .HasColumnType("uniqueidentifier"); + + b.Property("BodyContentType") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("BodySize") + .HasColumnType("int"); + + b.Property("BodyStoredExternally") + .HasColumnType("bit"); + + b.Property("BodyText") + .HasColumnType("nvarchar(max)"); + + b.Property("ConversationId") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("ExceptionMessage") + .HasColumnType("nvarchar(max)"); + + b.Property("ExceptionType") + .HasColumnType("nvarchar(max)"); + + b.Property("FailingEndpointAddress") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("FirstTimeOfFailure") + .HasColumnType("datetime2"); + + b.Property("HeadersJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSystemMessage") + .HasColumnType("bit"); + + b.Property("LastAttemptedAt") + .HasColumnType("datetime2"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastTimeOfFailure") + .HasColumnType("datetime2"); + + b.Property("MessageId") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("MessageType") + .HasColumnType("nvarchar(max)"); + + b.Property("NumberOfProcessingAttempts") + .HasColumnType("int"); + + b.Property("ReceivingEndpointHost") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("ReceivingEndpointHostId") + .HasColumnType("uniqueidentifier"); + + b.Property("ReceivingEndpointName") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("SendingEndpointHost") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("SendingEndpointHostId") + .HasColumnType("uniqueidentifier"); + + b.Property("SendingEndpointName") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StatusChangedAt") + .HasColumnType("datetime2"); + + b.Property("TimeSent") + .HasColumnType("datetime2"); + + b.HasKey("UniqueMessageId"); + + b.HasIndex("ConversationId"); + + b.HasIndex("FailingEndpointAddress"); + + b.HasIndex("ReceivingEndpointName"); + + b.HasIndex("StatusChangedAt") + .HasFilter("[Status] IN (2, 4)"); + + b.HasIndex("TimeSent"); + + b.HasIndex("Status", "LastModified"); + + b.ToTable("FailedMessages"); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageGroupEntity", b => + { + b.Property("FailedMessageUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("GroupId") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.HasKey("FailedMessageUniqueId", "GroupId"); + + b.HasIndex("GroupId"); + + b.HasIndex("Type", "GroupId"); + + b.ToTable("FailedMessageGroups"); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageRetryEntity", b => + { + b.Property("UniqueMessageId") + .HasColumnType("uniqueidentifier"); + + b.Property("RetryBatchId") + .HasColumnType("uniqueidentifier"); + + b.Property("StageAttempts") + .HasColumnType("int"); + + b.HasKey("UniqueMessageId"); + + b.HasIndex("RetryBatchId"); + + b.ToTable("FailedMessageRetries"); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.GroupCommentEntity", b => + { + b.Property("GroupId") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("GroupId"); + + b.ToTable("GroupComments"); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.HistoricRetryOperationEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompletionTime") + .HasColumnType("datetime2"); + + b.Property("Failed") + .HasColumnType("bit"); + + b.Property("NumberOfMessagesProcessed") + .HasColumnType("int"); + + b.Property("Originator") + .HasColumnType("nvarchar(max)"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("RetryType") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("CompletionTime", "Id") + .IsDescending(); + + b.ToTable("HistoricRetryOperations"); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.KnownEndpointEntity", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Host") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("HostId") + .HasColumnType("uniqueidentifier"); + + b.Property("Monitored") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.ToTable("KnownEndpoints"); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.MessageRedirectEntity", b => + { + b.Property("FromPhysicalAddress") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("ToPhysicalAddress") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.HasKey("FromPhysicalAddress"); + + b.ToTable("MessageRedirects"); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.RetryBatchEntity", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Classifier") + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("nvarchar(max)"); + + b.Property("InitialBatchSize") + .HasColumnType("int"); + + b.Property("InitiatedById") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("InitiatedByName") + .HasColumnType("nvarchar(max)"); + + b.Property("Last") + .HasColumnType("datetime2"); + + b.Property("OperationId") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("Originator") + .HasColumnType("nvarchar(max)"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("RetrySessionId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("RetryType") + .HasColumnType("int"); + + b.Property("StagingId") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Status", "RetrySessionId"); + + b.ToTable("RetryBatches"); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.RetryBatchNowForwardingEntity", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("RetryBatchId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.ToTable("RetryBatchNowForwarding"); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.SubscriptionEntity", b => + { + b.Property("MessageType") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("TransportAddress") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Endpoint") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.HasKey("MessageType", "TransportAddress"); + + b.ToTable("Subscriptions"); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.TrialMetadataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("TrialEndDate") + .HasColumnType("date"); + + b.HasKey("Id"); + + b.ToTable("TrialMetadata"); + + b.HasData( + new + { + Id = 1 + }); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.UnacknowledgedRetryOperationEntity", b => + { + b.Property("RequestId") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("RetryType") + .HasColumnType("int"); + + b.Property("Classifier") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("CompletionTime") + .HasColumnType("datetime2"); + + b.Property("Failed") + .HasColumnType("bit"); + + b.Property("Last") + .HasColumnType("datetime2"); + + b.Property("NumberOfMessagesProcessed") + .HasColumnType("int"); + + b.Property("Originator") + .HasColumnType("nvarchar(max)"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("RequestId", "RetryType"); + + b.ToTable("UnacknowledgedRetryOperations"); + }); + + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageGroupEntity", b => + { + b.HasOne("ServiceControl.Persistence.EFCore.Entities.FailedMessageEntity", null) + .WithMany() + .HasForeignKey("FailedMessageUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/ServiceControl.Persistence.EFCore.SqlServer/Migrations/20260804222033_AddRetryHistory.cs b/src/ServiceControl.Persistence.EFCore.SqlServer/Migrations/20260804222033_AddRetryHistory.cs new file mode 100644 index 0000000000..1492468775 --- /dev/null +++ b/src/ServiceControl.Persistence.EFCore.SqlServer/Migrations/20260804222033_AddRetryHistory.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ServiceControl.Persistence.EFCore.SqlServer.Migrations +{ + /// + public partial class AddRetryHistory : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "HistoricRetryOperations", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RequestId = table.Column(type: "nvarchar(400)", maxLength: 400, nullable: false), + RetryType = table.Column(type: "int", nullable: false), + StartTime = table.Column(type: "datetime2", nullable: false), + CompletionTime = table.Column(type: "datetime2", nullable: false), + Originator = table.Column(type: "nvarchar(max)", nullable: true), + Failed = table.Column(type: "bit", nullable: false), + NumberOfMessagesProcessed = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_HistoricRetryOperations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UnacknowledgedRetryOperations", + columns: table => new + { + RequestId = table.Column(type: "nvarchar(400)", maxLength: 400, nullable: false), + RetryType = table.Column(type: "int", nullable: false), + StartTime = table.Column(type: "datetime2", nullable: false), + CompletionTime = table.Column(type: "datetime2", nullable: false), + Last = table.Column(type: "datetime2", nullable: false), + Originator = table.Column(type: "nvarchar(max)", nullable: true), + Classifier = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: true), + Failed = table.Column(type: "bit", nullable: false), + NumberOfMessagesProcessed = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UnacknowledgedRetryOperations", x => new { x.RequestId, x.RetryType }); + }); + + migrationBuilder.CreateIndex( + name: "IX_HistoricRetryOperations_CompletionTime_Id", + table: "HistoricRetryOperations", + columns: new[] { "CompletionTime", "Id" }, + descending: new bool[0]); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "HistoricRetryOperations"); + + migrationBuilder.DropTable( + name: "UnacknowledgedRetryOperations"); + } + } +} diff --git a/src/ServiceControl.Persistence.EFCore.SqlServer/Migrations/SqlServerServiceControlDbContextModelSnapshot.cs b/src/ServiceControl.Persistence.EFCore.SqlServer/Migrations/SqlServerServiceControlDbContextModelSnapshot.cs index e2c68d1c5b..3023c5c092 100644 --- a/src/ServiceControl.Persistence.EFCore.SqlServer/Migrations/SqlServerServiceControlDbContextModelSnapshot.cs +++ b/src/ServiceControl.Persistence.EFCore.SqlServer/Migrations/SqlServerServiceControlDbContextModelSnapshot.cs @@ -323,6 +323,45 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("GroupComments"); }); + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.HistoricRetryOperationEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompletionTime") + .HasColumnType("datetime2"); + + b.Property("Failed") + .HasColumnType("bit"); + + b.Property("NumberOfMessagesProcessed") + .HasColumnType("int"); + + b.Property("Originator") + .HasColumnType("nvarchar(max)"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("RetryType") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("CompletionTime", "Id") + .IsDescending(); + + b.ToTable("HistoricRetryOperations"); + }); + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.KnownEndpointEntity", b => { b.Property("Id") @@ -484,6 +523,42 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.UnacknowledgedRetryOperationEntity", b => + { + b.Property("RequestId") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("RetryType") + .HasColumnType("int"); + + b.Property("Classifier") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("CompletionTime") + .HasColumnType("datetime2"); + + b.Property("Failed") + .HasColumnType("bit"); + + b.Property("Last") + .HasColumnType("datetime2"); + + b.Property("NumberOfMessagesProcessed") + .HasColumnType("int"); + + b.Property("Originator") + .HasColumnType("nvarchar(max)"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("RequestId", "RetryType"); + + b.ToTable("UnacknowledgedRetryOperations"); + }); + modelBuilder.Entity("ServiceControl.Persistence.EFCore.Entities.FailedMessageGroupEntity", b => { b.HasOne("ServiceControl.Persistence.EFCore.Entities.FailedMessageEntity", null) diff --git a/src/ServiceControl.Persistence.EFCore/DbContexts/ServiceControlDbContext.cs b/src/ServiceControl.Persistence.EFCore/DbContexts/ServiceControlDbContext.cs index 8973e49541..70c1ae4543 100644 --- a/src/ServiceControl.Persistence.EFCore/DbContexts/ServiceControlDbContext.cs +++ b/src/ServiceControl.Persistence.EFCore/DbContexts/ServiceControlDbContext.cs @@ -20,6 +20,8 @@ public abstract class ServiceControlDbContext(DbContextOptions options) : DbCont public DbSet TrialMetadata { get; set; } public DbSet Subscriptions { get; set; } public DbSet EventLogItems { get; set; } + public DbSet HistoricRetryOperations { get; set; } + public DbSet UnacknowledgedRetryOperations { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.EnableDetailedErrors(); @@ -42,6 +44,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfiguration(new SubscriptionConfiguration()); modelBuilder.ApplyConfiguration(new TrialMetadataConfiguration()); modelBuilder.ApplyConfiguration(new EventLogItemConfiguration()); + modelBuilder.ApplyConfiguration(new HistoricRetryOperationConfiguration()); + modelBuilder.ApplyConfiguration(new UnacknowledgedRetryOperationConfiguration()); } public abstract bool IsDuplicateKeyException(DbUpdateException exception); diff --git a/src/ServiceControl.Persistence.EFCore/Entities/HistoricRetryOperationEntity.cs b/src/ServiceControl.Persistence.EFCore/Entities/HistoricRetryOperationEntity.cs new file mode 100644 index 0000000000..42efb37e5c --- /dev/null +++ b/src/ServiceControl.Persistence.EFCore/Entities/HistoricRetryOperationEntity.cs @@ -0,0 +1,22 @@ +namespace ServiceControl.Persistence.EFCore.Entities; + +using ServiceControl.Persistence; + +public class HistoricRetryOperationEntity +{ + public long Id { get; set; } + + public required string RequestId { get; set; } + + public RetryType RetryType { get; set; } + + public DateTime StartTime { get; set; } + + public DateTime CompletionTime { get; set; } + + public string? Originator { get; set; } + + public bool Failed { get; set; } + + public int NumberOfMessagesProcessed { get; set; } +} diff --git a/src/ServiceControl.Persistence.EFCore/Entities/UnacknowledgedRetryOperationEntity.cs b/src/ServiceControl.Persistence.EFCore/Entities/UnacknowledgedRetryOperationEntity.cs new file mode 100644 index 0000000000..fa9fbcb4dc --- /dev/null +++ b/src/ServiceControl.Persistence.EFCore/Entities/UnacknowledgedRetryOperationEntity.cs @@ -0,0 +1,24 @@ +namespace ServiceControl.Persistence.EFCore.Entities; + +using ServiceControl.Persistence; + +public class UnacknowledgedRetryOperationEntity +{ + public required string RequestId { get; set; } + + public RetryType RetryType { get; set; } + + public DateTime StartTime { get; set; } + + public DateTime CompletionTime { get; set; } + + public DateTime Last { get; set; } + + public string? Originator { get; set; } + + public string? Classifier { get; set; } + + public bool Failed { get; set; } + + public int NumberOfMessagesProcessed { get; set; } +} diff --git a/src/ServiceControl.Persistence.EFCore/EntityConfigurations/ColumnLengths.cs b/src/ServiceControl.Persistence.EFCore/EntityConfigurations/ColumnLengths.cs index b8de0c3757..d66b0cddda 100644 --- a/src/ServiceControl.Persistence.EFCore/EntityConfigurations/ColumnLengths.cs +++ b/src/ServiceControl.Persistence.EFCore/EntityConfigurations/ColumnLengths.cs @@ -12,4 +12,9 @@ static class ColumnLengths // The subscriptions key spans two columns and SQL Server caps a clustered index key at 900 bytes, // so both have to stay well under ShortTextLength. Matches NServiceBus.Persistence.Sql. public const int SubscriptionKeyLength = 200; + + // The unacknowledged retry key is this column plus the RetryType int, and ShortTextLength would + // put it over SQL Server's 900 byte limit. 800 + 4 fits, with room for the queue addresses that + // ByQueueAddress retries use as their request id. + public const int RetryRequestIdLength = 400; } diff --git a/src/ServiceControl.Persistence.EFCore/EntityConfigurations/HistoricRetryOperationConfiguration.cs b/src/ServiceControl.Persistence.EFCore/EntityConfigurations/HistoricRetryOperationConfiguration.cs new file mode 100644 index 0000000000..3959623312 --- /dev/null +++ b/src/ServiceControl.Persistence.EFCore/EntityConfigurations/HistoricRetryOperationConfiguration.cs @@ -0,0 +1,22 @@ +namespace ServiceControl.Persistence.EFCore.EntityConfigurations; + +using Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +class HistoricRetryOperationConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(e => e.Id); + + builder.Property(e => e.RequestId).IsRequired().HasMaxLength(ColumnLengths.RetryRequestIdLength); + builder.Property(e => e.RetryType).IsRequired(); + builder.Property(e => e.StartTime).IsRequired(); + builder.Property(e => e.CompletionTime).IsRequired(); + + // The table is trimmed to the configured depth, so this serves the whole read as well as + // the trim's search for the cutoff row. + builder.HasIndex(e => new { e.CompletionTime, e.Id }).IsDescending(); + } +} diff --git a/src/ServiceControl.Persistence.EFCore/EntityConfigurations/UnacknowledgedRetryOperationConfiguration.cs b/src/ServiceControl.Persistence.EFCore/EntityConfigurations/UnacknowledgedRetryOperationConfiguration.cs new file mode 100644 index 0000000000..3483c43b13 --- /dev/null +++ b/src/ServiceControl.Persistence.EFCore/EntityConfigurations/UnacknowledgedRetryOperationConfiguration.cs @@ -0,0 +1,20 @@ +namespace ServiceControl.Persistence.EFCore.EntityConfigurations; + +using Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +class UnacknowledgedRetryOperationConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + // One row per pending acknowledgement: retrying the same operation again replaces it. + builder.HasKey(e => new { e.RequestId, e.RetryType }); + + builder.Property(e => e.RequestId).HasMaxLength(ColumnLengths.RetryRequestIdLength); + builder.Property(e => e.StartTime).IsRequired(); + builder.Property(e => e.CompletionTime).IsRequired(); + builder.Property(e => e.Last).IsRequired(); + builder.Property(e => e.Classifier).HasMaxLength(ColumnLengths.ShortTextLength); + } +} diff --git a/src/ServiceControl.Persistence.EFCore/Implementation/RetryHistoryDataStore.cs b/src/ServiceControl.Persistence.EFCore/Implementation/RetryHistoryDataStore.cs index bd514be05b..4170230ade 100644 --- a/src/ServiceControl.Persistence.EFCore/Implementation/RetryHistoryDataStore.cs +++ b/src/ServiceControl.Persistence.EFCore/Implementation/RetryHistoryDataStore.cs @@ -1,16 +1,151 @@ namespace ServiceControl.Persistence.EFCore.Implementation; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using ServiceControl.Persistence.EFCore.DbContexts; +using ServiceControl.Persistence.EFCore.Entities; using ServiceControl.Recoverability; -public class RetryHistoryDataStore : IRetryHistoryDataStore +public class RetryHistoryDataStore(IServiceScopeFactory scopeFactory) : DataStoreBase(scopeFactory), IRetryHistoryDataStore { public Task GetRetryHistory() => - throw new NotImplementedException(); + ExecuteWithDbContext(async dbContext => + { + var historicOperations = await dbContext.HistoricRetryOperations + .AsNoTracking() + .OrderByDescending(operation => operation.CompletionTime) + .ThenByDescending(operation => operation.Id) + .Select(operation => new HistoricRetryOperation + { + RequestId = operation.RequestId, + RetryType = operation.RetryType, + StartTime = operation.StartTime, + CompletionTime = operation.CompletionTime, + Originator = operation.Originator, + Failed = operation.Failed, + NumberOfMessagesProcessed = operation.NumberOfMessagesProcessed + }) + .ToListAsync(); + + var unacknowledgedOperations = await dbContext.UnacknowledgedRetryOperations + .AsNoTracking() + .Select(operation => new UnacknowledgedRetryOperation + { + RequestId = operation.RequestId, + RetryType = operation.RetryType, + StartTime = operation.StartTime, + CompletionTime = operation.CompletionTime, + Last = operation.Last, + Originator = operation.Originator, + Classifier = operation.Classifier, + Failed = operation.Failed, + NumberOfMessagesProcessed = operation.NumberOfMessagesProcessed + }) + .ToListAsync(); + + return new RetryHistory + { + HistoricOperations = historicOperations, + UnacknowledgedOperations = unacknowledgedOperations + }; + }); public Task RecordRetryOperationCompleted(string requestId, RetryType retryType, DateTime startTime, DateTime completionTime, string originator, string classifier, bool messageFailed, int numberOfMessagesProcessed, DateTime lastProcessed, int retryHistoryDepth) => - throw new NotImplementedException(); + ExecuteWithDbContext(async dbContext => + { + var strategy = dbContext.Database.CreateExecutionStrategy(); + + await strategy.ExecuteAsync(async () => + { + await using var transaction = await dbContext.Database.BeginTransactionAsync(); + + dbContext.HistoricRetryOperations.Add(new HistoricRetryOperationEntity + { + RequestId = requestId, + RetryType = retryType, + StartTime = startTime, + CompletionTime = completionTime, + Originator = originator, + Failed = messageFailed, + NumberOfMessagesProcessed = numberOfMessagesProcessed + }); + + if (NeedsAcknowledgement(retryType)) + { + await RecordUnacknowledged(dbContext, requestId, retryType, startTime, completionTime, + originator, classifier, messageFailed, numberOfMessagesProcessed, lastProcessed); + } + + await dbContext.SaveChangesAsync(); + + // After the insert, so the operation just recorded competes for a place in the history. + await TrimHistory(dbContext, retryHistoryDepth); + + await transaction.CommitAsync(); + }); + }); public Task AcknowledgeRetryGroup(string groupId) => - throw new NotImplementedException(); + ExecuteWithDbContext(async dbContext => + { + var acknowledged = await dbContext.UnacknowledgedRetryOperations + .Where(operation => operation.RequestId == groupId && operation.RetryType == RetryType.FailureGroup) + .ExecuteDeleteAsync(); + + return acknowledged > 0; + }); + + static bool NeedsAcknowledgement(RetryType retryType) => + retryType is not RetryType.SingleMessage and not RetryType.MultipleMessages; + + static async Task RecordUnacknowledged(ServiceControlDbContext dbContext, string requestId, RetryType retryType, + DateTime startTime, DateTime completionTime, string originator, string classifier, bool messageFailed, + int numberOfMessagesProcessed, DateTime lastProcessed) + { + var unacknowledged = await dbContext.UnacknowledgedRetryOperations + .SingleOrDefaultAsync(operation => operation.RequestId == requestId && operation.RetryType == retryType); + + if (unacknowledged == null) + { + unacknowledged = new UnacknowledgedRetryOperationEntity { RequestId = requestId, RetryType = retryType }; + dbContext.UnacknowledgedRetryOperations.Add(unacknowledged); + } + + unacknowledged.StartTime = startTime; + unacknowledged.CompletionTime = completionTime; + unacknowledged.Last = lastProcessed; + unacknowledged.Originator = originator; + unacknowledged.Classifier = classifier; + unacknowledged.Failed = messageFailed; + unacknowledged.NumberOfMessagesProcessed = numberOfMessagesProcessed; + } + + static async Task TrimHistory(ServiceControlDbContext dbContext, int retryHistoryDepth) + { + if (retryHistoryDepth <= 0) + { + await dbContext.HistoricRetryOperations.ExecuteDeleteAsync(); + return; + } + + // The oldest operation worth keeping. Everything ordering below it is over the depth. + var cutoff = await dbContext.HistoricRetryOperations + .AsNoTracking() + .OrderByDescending(operation => operation.CompletionTime) + .ThenByDescending(operation => operation.Id) + .Skip(retryHistoryDepth - 1) + .Select(operation => new { operation.CompletionTime, operation.Id }) + .FirstOrDefaultAsync(); + + if (cutoff == null) + { + return; + } + + await dbContext.HistoricRetryOperations + .Where(operation => operation.CompletionTime < cutoff.CompletionTime + || (operation.CompletionTime == cutoff.CompletionTime && operation.Id < cutoff.Id)) + .ExecuteDeleteAsync(); + } } diff --git a/src/ServiceControl.Persistence.Tests/EFCore/RetryHistoryDataStoreTests.cs b/src/ServiceControl.Persistence.Tests/EFCore/RetryHistoryDataStoreTests.cs new file mode 100644 index 0000000000..410c480412 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests/EFCore/RetryHistoryDataStoreTests.cs @@ -0,0 +1,222 @@ +namespace ServiceControl.Persistence.Tests; + +using System; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; +using ServiceControl.Recoverability; + +class RetryHistoryDataStoreTests : ErrorIngestionTestBase +{ + const int DefaultDepth = 10; + + static readonly DateTime Noon = new(2026, 8, 1, 12, 0, 0, DateTimeKind.Utc); + + [Test] + public async Task Returns_an_empty_history_when_nothing_has_completed() + { + var history = await RetryHistoryStore.GetRetryHistory(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(history.HistoricOperations, Is.Empty); + Assert.That(history.UnacknowledgedOperations, Is.Empty); + } + } + + [Test] + public async Task Records_a_completed_operation() + { + await RecordCompleted("group-1", originator: "OrderPlaced failures", classifier: "Exception Type and Stack Trace", + failed: true, numberOfMessagesProcessed: 3); + + var history = await RetryHistoryStore.GetRetryHistory(); + + var historic = history.HistoricOperations.Single(); + var unacknowledged = history.UnacknowledgedOperations.Single(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(historic.RequestId, Is.EqualTo("group-1")); + Assert.That(historic.RetryType, Is.EqualTo(RetryType.FailureGroup)); + Assert.That(historic.StartTime, Is.EqualTo(Noon.AddMinutes(-5))); + Assert.That(historic.CompletionTime, Is.EqualTo(Noon)); + Assert.That(historic.Originator, Is.EqualTo("OrderPlaced failures")); + Assert.That(historic.Failed, Is.True); + Assert.That(historic.NumberOfMessagesProcessed, Is.EqualTo(3)); + + Assert.That(unacknowledged.RequestId, Is.EqualTo("group-1")); + Assert.That(unacknowledged.RetryType, Is.EqualTo(RetryType.FailureGroup)); + Assert.That(unacknowledged.StartTime, Is.EqualTo(Noon.AddMinutes(-5))); + Assert.That(unacknowledged.CompletionTime, Is.EqualTo(Noon)); + Assert.That(unacknowledged.Last, Is.EqualTo(Noon.AddMinutes(-1))); + Assert.That(unacknowledged.Originator, Is.EqualTo("OrderPlaced failures")); + Assert.That(unacknowledged.Classifier, Is.EqualTo("Exception Type and Stack Trace")); + Assert.That(unacknowledged.Failed, Is.True); + Assert.That(unacknowledged.NumberOfMessagesProcessed, Is.EqualTo(3)); + } + } + + [Test] + public async Task Returns_the_newest_operations_first() + { + await RecordCompleted("group-1", completionTime: Noon); + await RecordCompleted("group-2", completionTime: Noon.AddHours(-1)); + await RecordCompleted("group-3", completionTime: Noon.AddHours(1)); + + var history = await RetryHistoryStore.GetRetryHistory(); + + Assert.That(history.HistoricOperations.Select(operation => operation.RequestId), + Is.EqualTo(new[] { "group-3", "group-1", "group-2" })); + } + + [Test] + public async Task Keeps_only_the_newest_operations_up_to_the_depth() + { + for (var minute = 0; minute < 5; minute++) + { + await RecordCompleted($"group-{minute}", completionTime: Noon.AddMinutes(minute), depth: 3); + } + + var history = await RetryHistoryStore.GetRetryHistory(); + + Assert.That(history.HistoricOperations.Select(operation => operation.RequestId), + Is.EqualTo(new[] { "group-4", "group-3", "group-2" })); + } + + [Test] + public async Task Breaks_ties_on_completion_time_by_the_order_recorded() + { + await RecordCompleted("group-1", completionTime: Noon, depth: 2); + await RecordCompleted("group-2", completionTime: Noon, depth: 2); + await RecordCompleted("group-3", completionTime: Noon, depth: 2); + + var history = await RetryHistoryStore.GetRetryHistory(); + + Assert.That(history.HistoricOperations.Select(operation => operation.RequestId), + Is.EqualTo(new[] { "group-3", "group-2" })); + } + + [Test] + public async Task Applies_a_reduced_depth_to_operations_already_recorded() + { + for (var minute = 0; minute < 4; minute++) + { + await RecordCompleted($"group-{minute}", completionTime: Noon.AddMinutes(minute)); + } + + await RecordCompleted("group-4", completionTime: Noon.AddMinutes(4), depth: 2); + + var history = await RetryHistoryStore.GetRetryHistory(); + + Assert.That(history.HistoricOperations.Select(operation => operation.RequestId), + Is.EqualTo(new[] { "group-4", "group-3" })); + } + + [Test] + public async Task Keeps_no_history_when_the_depth_is_zero() + { + await RecordCompleted("group-1"); + await RecordCompleted("group-2", depth: 0); + + var history = await RetryHistoryStore.GetRetryHistory(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(history.HistoricOperations, Is.Empty); + Assert.That(history.UnacknowledgedOperations, Has.Count.EqualTo(2), "acknowledgements are not subject to the history depth"); + } + } + + [TestCase(RetryType.SingleMessage)] + [TestCase(RetryType.MultipleMessages)] + public async Task Does_not_wait_for_an_acknowledgement_of_message_retries(RetryType retryType) + { + await RecordCompleted("request-1", retryType); + + var history = await RetryHistoryStore.GetRetryHistory(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(history.HistoricOperations, Has.Count.EqualTo(1)); + Assert.That(history.UnacknowledgedOperations, Is.Empty); + } + } + + [Test] + public async Task Replaces_the_pending_acknowledgement_when_a_group_is_retried_again() + { + await RecordCompleted("group-1", completionTime: Noon, numberOfMessagesProcessed: 3); + await RecordCompleted("group-1", completionTime: Noon.AddHours(1), numberOfMessagesProcessed: 7); + + var history = await RetryHistoryStore.GetRetryHistory(); + + var unacknowledged = history.UnacknowledgedOperations.Single(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(unacknowledged.CompletionTime, Is.EqualTo(Noon.AddHours(1))); + Assert.That(unacknowledged.NumberOfMessagesProcessed, Is.EqualTo(7)); + Assert.That(history.HistoricOperations, Has.Count.EqualTo(2), "the history keeps both completions"); + } + } + + [Test] + public async Task Keeps_the_pending_acknowledgements_of_other_retry_types_apart() + { + await RecordCompleted("request-1", RetryType.FailureGroup); + await RecordCompleted("request-1", RetryType.AllForEndpoint); + + var history = await RetryHistoryStore.GetRetryHistory(); + + Assert.That(history.UnacknowledgedOperations.Select(operation => operation.RetryType), + Is.EquivalentTo(new[] { RetryType.FailureGroup, RetryType.AllForEndpoint })); + } + + [Test] + public async Task Acknowledges_a_group_retry() + { + await RecordCompleted("group-1"); + + var acknowledged = await RetryHistoryStore.AcknowledgeRetryGroup("group-1"); + + var history = await RetryHistoryStore.GetRetryHistory(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(acknowledged, Is.True); + Assert.That(history.UnacknowledgedOperations, Is.Empty); + Assert.That(history.HistoricOperations, Has.Count.EqualTo(1), "acknowledging does not erase the history"); + } + } + + [Test] + public async Task Does_not_acknowledge_an_unknown_group() => + Assert.That(await RetryHistoryStore.AcknowledgeRetryGroup("group-1"), Is.False); + + [Test] + public async Task Does_not_acknowledge_an_operation_of_another_retry_type() + { + await RecordCompleted("SomeEndpoint", RetryType.AllForEndpoint); + + var acknowledged = await RetryHistoryStore.AcknowledgeRetryGroup("SomeEndpoint"); + + var history = await RetryHistoryStore.GetRetryHistory(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(acknowledged, Is.False); + Assert.That(history.UnacknowledgedOperations, Has.Count.EqualTo(1)); + } + } + + Task RecordCompleted(string requestId, RetryType retryType = RetryType.FailureGroup, DateTime? completionTime = null, + string originator = "OrderPlaced failures", string classifier = "Exception Type and Stack Trace", + bool failed = false, int numberOfMessagesProcessed = 1, int depth = DefaultDepth) + { + var completed = completionTime ?? Noon; + + return RetryHistoryStore.RecordRetryOperationCompleted(requestId, retryType, completed.AddMinutes(-5), completed, + originator, classifier, failed, numberOfMessagesProcessed, completed.AddMinutes(-1), depth); + } +} diff --git a/src/ServiceControl.Persistence.Tests/PersistenceTestBase.cs b/src/ServiceControl.Persistence.Tests/PersistenceTestBase.cs index abd4080fd7..bee2026528 100644 --- a/src/ServiceControl.Persistence.Tests/PersistenceTestBase.cs +++ b/src/ServiceControl.Persistence.Tests/PersistenceTestBase.cs @@ -111,6 +111,7 @@ protected static async Task WaitUntil(Func> conditionChecker, string protected IEventLogDataStore EventLogDataStore => ServiceProvider.GetRequiredService(); protected IFailedErrorImportDataStore FailedImportStore => ServiceProvider.GetRequiredService(); protected IRetryBatchStore RetryBatchStore => ServiceProvider.GetRequiredService(); + protected IRetryHistoryDataStore RetryHistoryStore => ServiceProvider.GetRequiredService(); protected ILicensingDataStore LicensingDataStore => ServiceProvider.GetRequiredService(); protected IQueueAddressStore QueueAddressStore => ServiceProvider.GetRequiredService(); protected IEndpointSettingsStore EndpointSettingsStore => ServiceProvider.GetRequiredService();