Skip to content

Add EF Core-backed failed message queries - #5671

Open
johnsimons wants to merge 3 commits into
masterfrom
john/query_failed_messages
Open

Add EF Core-backed failed message queries#5671
johnsimons wants to merge 3 commits into
masterfrom
john/query_failed_messages

Conversation

@johnsimons

Copy link
Copy Markdown
Member

Implements IFailedMessageQueryDataStore for the EF Core persisters, covering the failed message lists, the summary and the single-message lookups. Exception source and stack trace, and the Edited and EditOf markers, are read from the stored headers rather than duplicated into columns.

@johnsimons johnsimons self-assigned this Jul 31, 2026
@johnsimons
johnsimons force-pushed the john/query_failed_messages branch 4 times, most recently from 9d1bc40 to aa1a6fd Compare July 31, 2026 23:43

@rbev rbev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with a few suggestions

static readonly string[] PayloadColumns =
[
"MessageId", "MessageType", "TimeSent", "ConversationId", "QueueAddress",
"MessageId", "MessageType", "TimeSent", "ConversationId", "FailingEndpointAddress",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice that FailingEndpointAddress was added in a different PR. If this list having correct coverage of the entity is important there should be a test added for it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test

.AsNoTracking()
.FilterByStatus(status)
.FilterByLastModifiedRange(modified)
.FilterByQueueAddress(queueAddress)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be added to the Status, Modified index?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FailingEndpointAddress already has its own index, but the ToLower() below stops it being used, so adding the column to the composite wouldn't buy anything until that's resolved. See #5671 (comment)

public Task<QueryStatsInfo> GetFailedMessagesStats(string? status, string? modified, string? queueAddress) =>
ExecuteWithDbContext(dbContext => dbContext.FailedMessages
.AsNoTracking()
.FilterByStatus(status)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When status is null I'm pretty sure this will miss the index as the Where doesn't get added

@johnsimons johnsimons Aug 3, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at this and it seems the only place this method is called is from the

public async Task ErrorsHead(string status, string modified, string queueAddress)
which is never called from ServicePulse so I am hesitant in adding another index to the hot path ingestion table!

Comment on lines +34 to +41

continue;
}

if (Enum.TryParse<FailedMessageStatus>(filter, true, out var included))
{
includes.Add(included);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else instead of continue might read better.

Suggested change
continue;
}
if (Enum.TryParse<FailedMessageStatus>(filter, true, out var included))
{
includes.Add(included);
}
}
else
{
if (Enum.TryParse<FailedMessageStatus>(filter, true, out var included))
{
includes.Add(included);
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


foreach (var exclude in excludes)
{
// Captured per iteration so each exclusion closes over its own value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it just be !excludes.Contains(message.Status) instead of this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


var address = queueAddress.ToLowerInvariant();

// The ToLower() here causes a full table scan!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to avoid this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

queueAddress only arrives from the Pending Retries screen, which does poll every 5 seconds, so this isn't a cold path. But the predicate always runs after the status and modified filters, so it's a residual test over retry-issued messages in the selected range rather than a scan of the whole table, and that set is normally small. Not worth a schema change on that evidence. Worth noting the ToLower() is redundant on SQL Server anyway, since the default collation is already case-insensitive, and only PostgreSQL genuinely needs it, so if this ever does show up in a profile the fix is a normalised column rather than a new index.

var descending = sortInfo?.Direction != "asc";
var sort = sortInfo?.Sort;

if (sort == null || !SortInfo.AllowedSortOptions.Contains(sort))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this check redundant given the fallback in the switch statement?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Assert.That(row.TimeSent, Is.EqualTo(failure.TimeSent));
Assert.That(row.ConversationId, Is.EqualTo(failure.ConversationId));
Assert.That(row.QueueAddress, Is.EqualTo(failure.QueueAddress));
Assert.That(row.FailingEndpointAddress, Is.EqualTo(failure.QueueAddress));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this assert looks a bit sus

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

throw new NotImplementedException();
return (IDictionary<string, object>)new Dictionary<string, object>
{
["Endpoints"] = endpoints,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do these magic strings come from?
Are they magic strings on the raven side too?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes they are, I have made them more explicit and sharing between Raven and EF

Implements IFailedMessageQueryDataStore for the EF Core persisters, covering the failed message lists, the summary and the single-message lookups. Exception source and stack trace, and the Edited and EditOf markers, are read from the stored headers
rather than duplicated into columns.
@johnsimons
johnsimons force-pushed the john/query_failed_messages branch from 2c79e96 to ab1a1b5 Compare August 3, 2026 05:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants