-
Notifications
You must be signed in to change notification settings - Fork 51
Refactor and improve data versioning / etags #5794
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
Open
warwickschroeder
wants to merge
36
commits into
master
Choose a base branch
from
warwick/data-ver
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
7bc096d
Add DataVersion, one representation for a persisted query result version
warwickschroeder c40cc22
Carry the message body version as a DataVersion
warwickschroeder 0e84986
Retype the query result validator as DataVersion
warwickschroeder 6e23697
Emit the store version verbatim on every endpoint
warwickschroeder bf13604
Make the ingestion and clock test helpers available to every backend
warwickschroeder d788c9a
Fix merge issues
warwickschroeder a8bc1fc
Move the message body version when the stored body is replaced
warwickschroeder 8e32ddb
Mark derived validators weak and compare them per RFC 9110
warwickschroeder fc2b93c
Derive projected versions from the rows they report on
warwickschroeder 5a0fa1d
- Cover a paged endpoint and the empty case in the conditional GET ac…
warwickschroeder 869a04b
- Fix Groups data versioning
warwickschroeder 8686d3f
Clarify some comments
warwickschroeder 3b9c32d
Add data version tests for messages view
warwickschroeder 5e3f032
Add data version tests for message redirects
warwickschroeder c47aa89
The old validator named only the historic request ids, so acknowledgi…
warwickschroeder 563836b
Clean after review
warwickschroeder cd3d0b2
Fix versioning paged results
warwickschroeder da50d71
Fix versioning issue for message view
warwickschroeder 5af79d1
Fix custom checks versioning
warwickschroeder 924cde1
Improve and add tests
warwickschroeder f9c7498
Remove the unused strong tag
warwickschroeder 233bed7
Refactor to use shared OverRows function
warwickschroeder e24e5b5
Fix ordering for retry history
warwickschroeder aac3aa4
Cleanup
warwickschroeder 74211ec
Abstract away the IsStale boolean for EF
warwickschroeder aed683c
Use paged data versioning for eventlogs
warwickschroeder a103b2a
add data version design doc
warwickschroeder e1e3348
Changes from review
warwickschroeder 7f21c13
Fix after rebase
warwickschroeder 53244b5
Final review changes
warwickschroeder 3d6cc21
Clean up the not required knownVersion function on EventLogs
warwickschroeder 3c62544
Clean not needed PagedQueryResults
warwickschroeder df91de9
Remove IsStale from shared persistence layer
warwickschroeder 523ad92
Clean known enpoints
warwickschroeder d6febd9
Review changes
warwickschroeder 14998d2
add etags to messages2
warwickschroeder 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # Data versioning design | ||
|
|
||
| ## What it is | ||
|
|
||
| A **data version** is the short opaque label a query result carries so that a client asking for it again can be told "nothing has changed" instead of being sent the whole answer. On the wire it is an HTTP entity-tag: the response carries `ETag`, the client sends it back as `If-None-Match`, and a matching request is answered `304 Not Modified` with no body. | ||
|
|
||
| One value type carries it end to end: `DataVersion` in `src/ServiceControl.Persistence/Infrastructure/DataVersion.cs`. Every persister produces one, `QueryStatsInfo.Version` carries it out of the persistence layer, and the Web API turns it into the header. It is a `readonly struct`, so `default` is a legitimate value and no variable of the type can be null. | ||
|
|
||
| This is the primary (error) instance only. The audit instance still carries a `string ETag` on its own `QueryStatsInfo` and has not been converted. | ||
|
|
||
| ## The one rule | ||
|
|
||
| **If a field the response renders can change without the version changing, a client caches that page for ever and nothing reveals it.** No log line, no exception, no failing test. | ||
|
|
||
| The promise is scoped to **one URL**, because a client only ever sends a validator back to the URL that issued it. So what must never happen is one URL answering `304` when its own body would have differed. Two different URLs sharing a value is harmless: an HTTP cache is keyed on the whole URL. | ||
|
|
||
| That scoping is what makes a backend's own token usable. RavenDB's result etag stands for the state of the index behind the query, so it moves on any write the query could see, but it says nothing about which page was asked for: every `/api/errors` URL shares one value, whatever the page, sort or filter. The EF Core persisters compose over the rows they returned, so theirs differ per page. Both satisfy the rule. | ||
|
|
||
| ## Making one | ||
|
|
||
| | Factory | Use it for | | ||
| | --------------------------------------- | ----------------------------------------------------------------------------------------------- | | ||
| | `FromToken(string)` / `FromToken(long)` | a token the backend already produces, such as a RavenDB index etag or document change vector | | ||
| | `Compose(terms)` | named terms over aggregates, where an aggregate provably moves with the fields it stands in for | | ||
| | `OverRows(summary, rows, fields)` | a list the response renders row by row: summary terms for the whole set, plus one term per row | | ||
| | `Combine(instances)` | one version for a result gathered from several instances | | ||
| | `FromClient(header)` | a validator a caller sent back, in any shape an old or current instance might emit | | ||
|
|
||
| `Compose` hashes its description through `DeterministicGuid.MakeId`, so the emitted tag is a GUID rather than the underlying values. | ||
|
|
||
| Term names and every field inside a row are **length prefixed**. Without that, free user text carrying a delimiter could make two different results digest identically: a failure group titled `x.y` with an empty `Type` would collide with one titled `x` whose `Type` is `y`. `Format` accepts strings, `bool`, `DateTime` and `DateTimeOffset` (both by ticks) and anything `IFormattable` under the invariant culture, and **throws** on anything else, because a type whose `ToString` is not a documented function of its content would pin the version silently. | ||
|
|
||
| `OverRows` names rows by position, so a caller whose query has no `ORDER BY` has to sort them first or the validator churns. | ||
|
|
||
| ## Absence | ||
|
|
||
| `DataVersion.None` is `default`, and it **matches nothing, not even itself**. Two parties that both know nothing have not established that nothing changed, so an empty-string validator matching itself would answer `304` for every request. | ||
|
|
||
| Absence propagates in the safe direction. `WithEtag` writes no header for `None`, so no header means no `If-None-Match`, which means the full body. `Combine` returns `None` as soon as any instance reports none, rather than quietly claiming to cover an instance it could not see. | ||
|
|
||
| ## Reaching the client | ||
|
|
||
| `WithEtag` emits **every** tag weak, as `W/"…"`. Nothing here can promise the response bytes: response compression rewrites them without touching the tag, and no endpoint enables range processing, which is the one thing an exact validator would buy. RFC 9110 requires `If-None-Match` to use the weak comparison anyway, so the marking costs nothing. | ||
|
|
||
| `NotModifiedStatusHttpHandler` turns a matching request into a `304`. It compares with `EntityTagHeaderValue.Compare(useStrongComparison: false)`, because `Equals` on that type compares strength as well as the tag and its own documentation says not to use it for this. `*` matches whenever a representation exists. | ||
|
|
||
| ## Across instances | ||
|
|
||
| Scatter-gather endpoints merge one version per instance through `Combine`. It is keyed on instance id and sorted ordinally, so the composite is independent of the order instances answered in but still moves if two instances swap which validator they report. | ||
|
|
||
| An API whose own instance holds none of the data drops its own empty result before aggregating, via `AggregateStatsFromRemotesOnly`. Left in, its version-less placeholder would take the whole composite to `None` and the endpoint would emit no tag at all. |
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
15 changes: 10 additions & 5 deletions
15
src/ServiceControl.Audit.Persistence.RavenDB/Extensions/RavenQueryStatisticsExtensions.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 |
|---|---|---|
| @@ -1,13 +1,18 @@ | ||
| namespace ServiceControl.Audit.Persistence.RavenDB.Extensions | ||
| { | ||
| using System.Globalization; | ||
| using Auditing.MessagesView; | ||
| using Raven.Client.Documents.Session; | ||
|
|
||
| static class RavenQueryStatisticsExtensions | ||
| { | ||
| public static QueryStatsInfo ToQueryStatsInfo(this QueryStatistics stats) | ||
| { | ||
| return new QueryStatsInfo($"{stats.ResultEtag}", stats.TotalResults); | ||
| } | ||
| /// <summary> | ||
| /// RavenDB's result etag hashes the state of the index behind the query: every collection's last | ||
| /// document and tombstone etag, how far the index has processed, and its definition. It therefore | ||
| /// moves on any write the query could see, which is all a validator has to do, because a client only | ||
| /// ever sends one back on a request for the same URL. | ||
| /// </summary> | ||
| public static QueryStatsInfo ToQueryStatsInfo(this QueryStatistics stats) => | ||
| new(stats.ResultEtag?.ToString(CultureInfo.InvariantCulture) ?? string.Empty, stats.TotalResults); | ||
| } | ||
| } | ||
| } |
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
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@warwickschroeder I haven't done a thorough review yet, but this one seems wrong. We don't need to add another column to this table, as we previously mentioned; bodies are immutable. In other words, the body would only change if a different
UniqueMessageIdwas issued.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@johnsimons from what I'm reading, it doesnt seem to be immutable. The failed message is set via an upsert, which also includes the body text.
It looks like if an already retried message is edited and retried again, the body would be updated due to it checking for the originals header?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An edit creates a brand new message.
I am 100% sure that bodies are immutable.
If we are updating the body as part of an upsert, we should not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just reviewed the upsert, I think because we don't know whether it is going to be an insert or update, we still need to send the body regardless, but we could skip updating the body if it is an update.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I can look into doing that so it is 100% immutable. Then I can remove the additional field.