-
Notifications
You must be signed in to change notification settings - Fork 115
Test handling of multiple verdicts with the same update_id #7007
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
base: main
Are you sure you want to change the base?
Changes from 5 commits
04e824a
022e5f1
0472ed5
4b57fcc
68f0f8b
7bb15a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,24 @@ | ||
| package org.lfdecentralizedtrust.splice.scan.automation | ||
|
|
||
| import com.digitalasset.canton.BaseTest | ||
| import com.digitalasset.canton.data.CantonTimestamp | ||
| import org.scalatest.matchers.should.Matchers | ||
| import com.digitalasset.canton.logging.SuppressionRule | ||
| import com.digitalasset.canton.mediator.admin.v30 | ||
| import org.scalatest.wordspec.AnyWordSpec | ||
| import org.slf4j.event.Level.INFO | ||
|
|
||
| class ScanVerdictIngestionServiceTest extends AnyWordSpec with Matchers { | ||
| class ScanVerdictIngestionServiceTest extends AnyWordSpec with BaseTest { | ||
|
|
||
| private def ts(micros: Long) = CantonTimestamp.ofEpochMicro(micros) | ||
|
|
||
| private def mkVerdict(updateId: String, accepted: Boolean): v30.Verdict = | ||
| v30.Verdict.defaultInstance.copy( | ||
| updateId = updateId, | ||
| verdict = | ||
| if (accepted) v30.VerdictResult.VERDICT_RESULT_ACCEPTED | ||
| else v30.VerdictResult.VERDICT_RESULT_REJECTED, | ||
| ) | ||
|
|
||
| "findMissingTrafficSummaries" should { | ||
|
|
||
| "return empty when ingestion hasn't started" in { | ||
|
|
@@ -50,4 +61,79 @@ class ScanVerdictIngestionServiceTest extends AnyWordSpec with Matchers { | |
| ) shouldBe empty | ||
| } | ||
| } | ||
|
|
||
| "findDuplicateUpdateIds" should { | ||
|
|
||
| "return empty when all update ids are distinct" in { | ||
| ScanVerdictIngestionService.findDuplicateUpdateIds( | ||
| Seq(mkVerdict("a", true), mkVerdict("b", false)) | ||
| ) shouldBe empty | ||
| } | ||
|
|
||
| "return only the update ids that appear more than once" in { | ||
| val result = ScanVerdictIngestionService.findDuplicateUpdateIds( | ||
| Seq(mkVerdict("a", true), mkVerdict("b", false), mkVerdict("a", false)) | ||
| ) | ||
| result.keySet shouldBe Set("a") | ||
| result("a").map(_._2) shouldBe Seq(0, 2) | ||
| } | ||
| } | ||
|
|
||
| "duplicatesContainAccept" should { | ||
|
|
||
| "return false when no duplicate is an accept" in { | ||
| val duplicates = ScanVerdictIngestionService.findDuplicateUpdateIds( | ||
| Seq(mkVerdict("a", false), mkVerdict("a", false)) | ||
| ) | ||
| ScanVerdictIngestionService.duplicatesContainAccept(duplicates) shouldBe false | ||
| } | ||
|
|
||
| "return true when a duplicate group contains an accept" in { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This tests accept-after-reject. Add the same test for reject-after-accept. |
||
| val duplicates = ScanVerdictIngestionService.findDuplicateUpdateIds( | ||
| Seq(mkVerdict("a", false), mkVerdict("a", true)) | ||
| ) | ||
| ScanVerdictIngestionService.duplicatesContainAccept(duplicates) shouldBe true | ||
| } | ||
|
|
||
| "ignore an accept that is not part of a duplicate group" in { | ||
| // "a" is accepted but unique; only "b" is duplicated (both rejected). | ||
| val duplicates = ScanVerdictIngestionService.findDuplicateUpdateIds( | ||
| Seq(mkVerdict("a", true), mkVerdict("b", false), mkVerdict("b", false)) | ||
| ) | ||
| ScanVerdictIngestionService.duplicatesContainAccept(duplicates) shouldBe false | ||
| } | ||
| } | ||
|
|
||
| "logDuplicateUpdateIds" should { | ||
|
|
||
| "not log when there are no duplicates" in { | ||
| loggerFactory.assertLogsSeq(SuppressionRule.LevelAndAbove(INFO))( | ||
| ScanVerdictIngestionService.logDuplicateUpdateIds( | ||
| Seq(mkVerdict("a", true), mkVerdict("b", true)), | ||
| logger, | ||
| ), | ||
| _ shouldBe empty, | ||
| ) | ||
| } | ||
|
|
||
| "log at info level when no duplicate is an accept" in { | ||
| loggerFactory.assertLogs(SuppressionRule.LevelAndAbove(INFO))( | ||
| ScanVerdictIngestionService.logDuplicateUpdateIds( | ||
| Seq(mkVerdict("a", false), mkVerdict("a", false)), | ||
| logger, | ||
| ), | ||
| _.infoMessage should include("Duplicate verdicts:"), | ||
| ) | ||
| } | ||
|
|
||
| "log at warning level when a duplicate is an accept" in { | ||
| loggerFactory.assertLogs( | ||
| ScanVerdictIngestionService.logDuplicateUpdateIds( | ||
| Seq(mkVerdict("a", true), mkVerdict("a", false)), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As noted above, this particular order should not warn. |
||
| logger, | ||
| ), | ||
| _.warningMessage should endWith("Duplicate verdicts contains an accept."), | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -412,6 +412,101 @@ class DbAppActivityRecordStoreTest | |
| countAfter shouldBe 0L | ||
| } | ||
| } | ||
|
|
||
| "drop a rejected verdict that's a duplicate of a prior accept" in { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here we already have the behavior that reject-after-accept does not warn. |
||
| val updateId = "update-dupe-reject-after-accept" | ||
| val ts1 = CantonTimestamp.now() | ||
| val ts2 = ts1.plusSeconds(1L) | ||
| for { | ||
| (appStore, verdictStore) <- newStores() | ||
| accepted = mkVerdict( | ||
| verdictStore, | ||
| updateId, | ||
| ts1, | ||
| DbScanVerdictStore.VerdictResultDbValue.Accepted, | ||
| ) | ||
| rejected = mkVerdict( | ||
| verdictStore, | ||
| updateId, | ||
| ts2, | ||
| DbScanVerdictStore.VerdictResultDbValue.Rejected, | ||
| ) | ||
| // First batch with the accepted verdict and its activity record | ||
| _ <- verdictStore.insertVerdictsWithAppActivityRecords( | ||
| NonEmptyList.of(accepted -> noViews), | ||
| Seq(ts1 -> mkRecord(0L, 10L, Seq("app1::provider"), Seq(100L))), | ||
| hasTrafficSummaries = true, | ||
| firstActiveRoundO = Some(10L), | ||
| lastArchivedRoundO = Some(9L), | ||
| ) | ||
| countAfterBatch1 <- countRecords() | ||
| // A later batch with a rejection for the same update_id | ||
| _ <- verdictStore.insertVerdictsWithAppActivityRecords( | ||
| NonEmptyList.of(rejected -> noViews), | ||
| Seq(ts2 -> mkRecord(0L, 11L, Seq("app2::provider"), Seq(200L))), | ||
| hasTrafficSummaries = true, | ||
| firstActiveRoundO = Some(11L), | ||
| lastArchivedRoundO = Some(10L), | ||
| ) | ||
| v <- verdictStore.getVerdictByUpdateId(updateId) | ||
| countAfterBatch2 <- countRecords() | ||
| } yield { | ||
| v shouldBe defined | ||
| v.value.verdictResult shouldBe DbScanVerdictStore.VerdictResultDbValue.Accepted | ||
| v.value.recordTime shouldBe ts1 | ||
| // The rejection's activity record was dropped along with the verdict | ||
| countAfterBatch2 shouldBe countAfterBatch1 | ||
| } | ||
| } | ||
|
|
||
| "drop and warn of an accepted verdict that's a duplicate of a prior rejection" in { | ||
| val updateId = "update-dupe-accept-after-reject" | ||
| val ts1 = CantonTimestamp.now() | ||
| val ts2 = ts1.plusSeconds(1L) | ||
| for { | ||
| (appStore, verdictStore) <- newStores() | ||
| rejected = mkVerdict( | ||
| verdictStore, | ||
| updateId, | ||
| ts1, | ||
| DbScanVerdictStore.VerdictResultDbValue.Rejected, | ||
| ) | ||
| accepted = mkVerdict( | ||
| verdictStore, | ||
| updateId, | ||
| ts2, | ||
| DbScanVerdictStore.VerdictResultDbValue.Accepted, | ||
| ) | ||
| // First batch with the rejection and its activity record | ||
| _ <- verdictStore.insertVerdictsWithAppActivityRecords( | ||
| NonEmptyList.of(rejected -> noViews), | ||
| Seq(ts1 -> mkRecord(0L, 10L, Seq("app1::provider"), Seq(100L))), | ||
| hasTrafficSummaries = true, | ||
| firstActiveRoundO = Some(10L), | ||
| lastArchivedRoundO = Some(9L), | ||
| ) | ||
| countAfterBatch1 <- countRecords() | ||
| // A later batch with an accept for the same update_id | ||
| _ <- loggerFactory.assertLogs( | ||
| verdictStore.insertVerdictsWithAppActivityRecords( | ||
| NonEmptyList.of(accepted -> noViews), | ||
| Seq(ts2 -> mkRecord(0L, 11L, Seq("app2::provider"), Seq(200L))), | ||
| hasTrafficSummaries = true, | ||
| firstActiveRoundO = Some(11L), | ||
| lastArchivedRoundO = Some(10L), | ||
| ), | ||
| _.warningMessage should startWith("Dropping duplicate accepted verdicts"), | ||
| ) | ||
| v <- verdictStore.getVerdictByUpdateId(updateId) | ||
| countAfterBatch2 <- countRecords() | ||
| } yield { | ||
| v shouldBe defined | ||
| v.value.verdictResult shouldBe DbScanVerdictStore.VerdictResultDbValue.Rejected | ||
| v.value.recordTime shouldBe ts1 | ||
| // The accept's activity record was dropped along with the verdict | ||
| countAfterBatch2 shouldBe countAfterBatch1 | ||
| } | ||
| } | ||
| } | ||
|
|
||
| "earliestRoundWithCompleteAppActivity" should { | ||
|
|
@@ -1200,6 +1295,7 @@ class DbAppActivityRecordStoreTest | |
| verdictStore: DbScanVerdictStore, | ||
| updateId: String, | ||
| recordTs: CantonTimestamp, | ||
| verdictResult: Short = DbScanVerdictStore.VerdictResultDbValue.Accepted, | ||
| ): verdictStore.VerdictT = | ||
| new verdictStore.VerdictT( | ||
| rowId = 0L, | ||
|
|
@@ -1208,7 +1304,7 @@ class DbAppActivityRecordStoreTest | |
| recordTime = recordTs, | ||
| finalizationTime = recordTs, | ||
| submittingParticipantUid = "participant1", | ||
| verdictResult = DbScanVerdictStore.VerdictResultDbValue.Accepted, | ||
| verdictResult = verdictResult, | ||
| mediatorGroup = 0, | ||
| updateId = updateId, | ||
| submittingParties = Seq.empty, | ||
|
|
||
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.
This looks off. We should only warn if we see an accept after seeing another verdict for the same update id.
Seeing a reject after an accept is fine and should not trigger a warning (and thus a production alert).
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.
Got it. I renamed this
duplicatesContainSubsequentAccept, tweaked the logic, and updated the tests accordingly.