Extract the page-update rules from PageUpdateManager and inject IClock (#369 part 2a) - #397
Open
sambetts wants to merge 2 commits into
Open
Extract the page-update rules from PageUpdateManager and inject IClock (#369 part 2a)#397sambetts wants to merge 2 commits into
sambetts wants to merge 2 commits into
Conversation
#369 part 2a) First of two PRs finishing #369. This one lifts PageUpdateManager's pure decisions into testable classes and injects IClock; the persistence ports and HitPatch follow in 2b. Four new classes under WebJob.AppInsightsImporter.Engine/PageUpdates/Rules/: * PageUpdateGroupingRules.GroupByUrl - buckets a chunk of page-update events by their URL's base address, case-insensitively, dropping events with no usable URL. Moved verbatim, including the OrdinalIgnoreCase comparer that stops a casing-only difference creating a second urls row. * PageUpdateRefreshPolicy.StaleBeforeUtc - the metadata refresh-suppression window. Takes the instant as a parameter, per the ImportCadenceGate.ShouldRun(..., DateTime nowUtc) convention. * PageUserEventRules - validation and de-duplication of the comments and likes carried on a page-update event, returning decisions in input order so the caller's log sequence and partial-failure behaviour are unchanged; plus the sentiment-enrichment gate. * UrlMetadataPropertyRules.IsImportableSimpleProp - the "ignore system and over-sized fields" filter, moved verbatim (including its culture-sensitive StartsWith and its NRE on a null field name, both pre-existing and both deliberately unchanged). PageUpdateManager now takes an optional IClock (defaulting to SystemClock.Instance) and uses it for all six DateTime.UtcNow sites it owned: the refresh cutoff, the MetadataLastRefreshed stamp, the two FileMetadataPropertyValue.Updated stamps, and the comment/like Created fallbacks. One deliberate divergence: the staleness cutoff is now computed in C# and passed as a parameter, instead of DbFunctions.AddMinutes(DateTime.UtcNow, -N) computing it in SQL. Same instant; the only difference is that EF6 now types the parameter from the mapped column (datetime) rather than as datetime2, so the cutoff can be rounded by up to 3.33 ms - against a default staleness window of 24 hours. Tests: 17 new in AppInsightsPageUpdateRulesTests, zero SQL Server / cognitive services / wall clock. Pre-existing DB-backed evidence still passing unchanged: PageUpdateManagerTests, PageUpdateManagerMultithreadTests, PageUpdateManagerMultithreadSamePropNameValTests, PageUpdateManagerSamePropNameValTests, CommentsSaveTests, PageUpdateManagerCommentsAndLikesTests, DupTaxonomoyPropertiesWithDifferentGuidsPageUpdateManagerTests, both merge tests and both PageUpdateManagerPerfTests - 111 of 112 in the AppInsights/PageUpdate area, the one failure being the pre-existing CommentsCognitiveTests DNS failure against the placeholder local cognitive endpoint. Addresses #369 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rong The reviewer checked the EF6 mapping and my justification does not hold. EF6 does NOT evaluate a bare DateTime.UtcNow inside a LINQ-to-Entities query on the client - it maps it to the canonical function CurrentUtcDateTime(), which SQL Server renders as SysUtcDateTime(). So the old predicate compared against the DATABASE SERVER's clock, and the new one compares against the WEB JOB HOST's clock. The real change is therefore the clock SOURCE, not datetime-vs-datetime2 parameter typing as I claimed. The impact is bounded by app-to-database clock skew (sub-second on NTP-synced Azure) against a 24-hour staleness window, and it is arguably more self-consistent because MetadataLastRefreshed is stamped from the host clock in SaveAll - so the comparison now uses one clock instead of two. Worth noting there is no form of this predicate that keeps the database clock AND is injectable: passing _clock.UtcNow to DbFunctions.AddMinutes parameterises the host clock just the same. Code unchanged; the XML doc now records this accurately. Also dropped PageUpdateRefresh_ZeroWindow_..., which the reviewer correctly called near-tautological (AddMinutes(0) is identity, and the sign/unit regression it might catch is already covered). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
First of two PRs finishing #369 (the App Insights importer half of #381). This one lifts
PageUpdateManager's pure decisions into testable classes and injectsIClock. The persistence ports (I*PersistenceManager,IImportDbMaintenance),HitPatch/HitUpdatesSqlExtensionandPageViewSaveResultfollow in part 2b — this PR touches no SQL, no staging schema and no merge script.Part 1 (PR #386) extracted
PageViewStagingRules,SearchTermRulesandClickEventRulesfrom the save extensions. This PR does the same job for the page-update manager, which the issue calls out as the largest remaining piece.What moved
Four new classes under
WebJob.AppInsightsImporter.Engine/PageUpdates/Rules/:PageUpdateGroupingRules.GroupByUrlStringComparer.OrdinalIgnoreCasecomparer that stops a casing-only difference producing a secondurlsrow — the same class of bug as #380/#385. Also keeps the "build the buckets once" property that replaced an O(events × urls) scan.PageUpdateRefreshPolicy.StaleBeforeUtcPageUserEventRulesUrlMetadataPropertyRules.IsImportableSimplePropPageUpdateManagernow takes an optionalIClock(#368), defaulting toSystemClock.Instance, and uses it for all sixDateTime.UtcNowsites it owned: the refresh cutoff, theMetadataLastRefreshedstamp inSaveAll, the twoFileMetadataPropertyValue.Updatedstamps, and theCreatedfallbacks for a comment and for a like. Both constructors gained the parameter as a trailing optional, so no call site breaks.Ordering is preserved deliberately
ProcessCustomAppInsightsEventsused to interleave "log this invalid event" and "create this new record" as it walked the input.PageUserEventRules.Classifytherefore returns decisions in input order and the caller still walks them one at a time, so:WARNING: Invalid comment/like metadata in event: …lines appear in the same sequence relative to the record creations;Pre-classifying up front is safe because
dbValuesis a materialised list from the database that the callbacks never mutate — they add todb.UrlLikes/ thenewCommentsdictionary, not to it.Two pieces of existing behaviour are documented rather than "fixed", because #369 forbids behavioural change:
(url, sp_id)— a user really can comment twice on a page);UrlMetadataPropertyRules.IsImportableSimplePropis likewise verbatim, including two pre-existing quirks I deliberately did not change and have documented in its XML comment: anullfield name throws (unreachable from the importer — a JSON object cannot have a null key), andStartsWith(string)is the culture-sensitive overload.The one deliberate divergence
The staleness cutoff is now computed in C# and passed as a parameter:
Corrected after review — my first explanation of this was wrong. EF6 does not evaluate a bare
DateTime.UtcNowinside a LINQ-to-Entities query on the client: it maps it to the canonical functionCurrentUtcDateTime(), which the SQL Server provider renders asSysUtcDateTime(). So the old predicate compared against the database server's clock; the new one compares against the web-job host's clock.The real change is therefore the clock source, not
datetime-vs-datetime2parameter typing. The impact is bounded by app-to-database clock skew — sub-second on NTP-synced Azure — against a 24-hour staleness window, and it is arguably more self-consistent:MetadataLastRefreshedis stamped from the host clock inSaveAll, so the comparison now uses one clock rather than two. Note there is no form of this predicate that keeps the database clock and is injectable — passing_clock.UtcNowintoDbFunctions.AddMinutesparameterises the host clock just the same. ThePageUpdateRefreshPolicyXML doc records this.Testing
New: 16 tests in
AppInsightsPageUpdateRulesTests— zero SQL Server, zero cognitive services, zero wall clock. What each pins:#fragmentare ignored; events with no usable URL are dropped; bucket order is preserved (the compile step takesName/Usernamefrom the first update); a Greek SharePoint URL is grouped rather than dropped;vti_x005f…is dropped butvti_…is not, and a Greek field name is kept;nullnormalised e-mail; the e-mail is lower-cased for the user lookup; an already-stored SharePoint id is not recreated; decisions come back in input order; likes classify identically to comments;Pre-existing, unchanged, still passing (DB-backed):
PageUpdateManagerTests,PageUpdateManagerMultithreadTests,PageUpdateManagerMultithreadSamePropNameValTests,PageUpdateManagerSamePropNameValTests,CommentsSaveTests,PageUpdateManagerCommentsAndLikesTests,DupTaxonomoyPropertiesWithDifferentGuidsPageUpdateManagerTests,PageUpdateEventAppInsightsQueryResultMergeTest,PageUpdateEventAppInsightsQueryResultCommentsAndLikesMergeTest, and bothPageUpdateManagerPerfTests.Area run: 111 passed / 1 failed of 112 — the failure is the pre-existing
CommentsCognitiveTests, which cannot resolve the placeholderexample-cognitive.cognitiveservices.azure.comendpoint in the local dev config and passes in CI. Full solution builds (Debug).Reviewer hotspots
datetimeparameter-typing divergence above — is the ≤3.33 ms rounding acceptable, or should theDbFunctions.AddMinutesform be restored?PageUserEventRules.Classifyversus the original loop — in particular that pre-classifying cannot observe a staledbValues, and that ordering/partial-failure semantics really are identical.Math.Max-grade arithmetic; call out anything that reads as theatre and I will delete it rather than pad it.Still to come in part 2b
IPageViewsPersistenceManager/ISearchesPersistenceManager/IClicksPersistenceManager/IPageUpdatePersistenceManager/IHitUpdatePersistenceManager/IImportDbMaintenancewithSql*adapters and in-memory fakes,PageViewSaveResultcarrying the countsPageViewStagingPlanalready computes, and removingnew SPOInsightsEntitiesContext()+Console.WriteLinefromHitPatch.cs.IImportDbMaintenanceis shared with #374, so it will be defined once — whichever of the two lands first.Addresses #369