Add PruneRewardAccountingTrigger to delete old reward-accounting data - #6874
Add PruneRewardAccountingTrigger to delete old reward-accounting data#6874dfordivam wants to merge 4 commits into
Conversation
| /** Deletes all reward-accounting data for a single round across all six | ||
| * tables, in a single transaction. | ||
| */ | ||
| def deleteRewardAccountingDataForRound( |
There was a problem hiding this comment.
Indexes being used here for reference
"app_reward_batch_hashes_pkey" PRIMARY KEY, btree (history_id, round_number, batch_level, party_seq_num_begin_incl)
"app_reward_root_hashes_pkey" PRIMARY KEY, btree (history_id, round_number)
"app_reward_party_totals_pkey" PRIMARY KEY, btree (history_id, round_number, app_provider_party_seq_num)
"app_reward_round_totals_pkey" PRIMARY KEY, btree (history_id, round_number)
"app_activity_party_totals_pkey" PRIMARY KEY, btree (history_id, round_number, app_provider_party)
"app_activity_round_totals_pkey" PRIMARY KEY, btree (history_id, round_number)
| } | ||
|
|
||
| // Returns number of rows deleted | ||
| def pruneArchivedDataForRound( |
There was a problem hiding this comment.
This will always be a seq scan
| summary <- appRewardsStore.deleteRewardAccountingDataForRound(task.roundNumber) | ||
| deletedArchiveRows <- rewardsReferenceStore.pruneArchivedDataForRound(task.roundNumber) | ||
| } yield TaskSuccess( | ||
| s"Pruned reward accounting data for round ${task.roundNumber}: " + |
There was a problem hiding this comment.
Example log
"Completed processing with outcome: Pruned reward accounting data for round 11: removed 5 archived rows; and RewardAccountingPruneSummary(2,1,2,1,1,1)."
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
a82c334 to
8d8d439
Compare
| Seq(Task(roundNumber)) | ||
| } else { | ||
| logger.debug( | ||
| s"Skipping pruning of round $roundNumber as the ingestion's currently active round " + |
There was a problem hiding this comment.
This is the log, though this may be noisy and perhaps can be removed
"Skipping pruning of round 12 as the ingestion's currently active round 15 opened at 1970-01-01T17:47:20Z, which is not yet past round 13's archivedAt (1970-01-01T17:47:30.000228Z)."
| case _ => Future.successful(None) | ||
| } | ||
|
|
||
| def lookupArchivedAtForOpenMiningRound( |
There was a problem hiding this comment.
nit: use override here, to be consistent with lookupLowestPrunableArchiveRewardRound below
Also for pruneArchiveDataForRound
| sv2HistoryId, | ||
| sv2RewardsRefStore, | ||
| newLowestOpen, | ||
| timeUntilSuccess = 90.seconds, |
There was a problem hiding this comment.
The comment above suggests that it can complete in 20s, but is sometimes much longer; is it possible to also document why there is such a large variance as a means of justifying the 90s ?
There was a problem hiding this comment.
Also interested in knowing why it takes so long. Is it verdict ingestion taking so long to catch up, or the pruning trigger?
There was a problem hiding this comment.
It is verdict ingestion, the cause is some grpc stream issue where it errors out if not consumed, and it need retry by the service.
|
|
||
| import scala.concurrent.{ExecutionContext, Future} | ||
|
|
||
| /** Periodically deletes reward-accounting data that is no longer needed, one |
There was a problem hiding this comment.
Design question: it's not mentioned in #6136, so asking here: is it confirmed that no retention window is necessary? The data in these tables is useful for debugging. IUC, this prunes them aggressively - should there be a configurable capability to retain them for a bit longer?
There was a problem hiding this comment.
Good point! I think a configurable retention window with a default of 1 week (or at least a day) would be very useful for debugging. In case of a high severity issue like a root hash mismatch we might want to inspect the state of the database, and it might take a while to get a data dump from all SVs.
rautenrieth-da
left a comment
There was a problem hiding this comment.
Thanks! Wasn't easy to review, but looking good in general. Left some minor comments.
| clue( | ||
| s"sv2 does not prune rounds below $newLowestOpen while its verdict ingestion is paused" | ||
| ) { | ||
| hasUnprunedArchiveDataForRound(sv2RewardsRefStore, newLowestOpen - 1) shouldBe true |
There was a problem hiding this comment.
Should we explicitly trigger one iteration of PruneRewardAccountingTrigger here? This might mean that we have to pause the trigger for the entire duration of the test and control them manually. Although unlikely, it could be that sv2 has unpruned data simply because the trigger didn't finish yet.
| sv2HistoryId, | ||
| sv2RewardsRefStore, | ||
| newLowestOpen, | ||
| timeUntilSuccess = 90.seconds, |
There was a problem hiding this comment.
Also interested in knowing why it takes so long. Is it verdict ingestion taking so long to catch up, or the pruning trigger?
| .lookupActiveOpenMiningRounds(Seq(lastIngestedRecordTime)) | ||
| .flatMap(_.get(lastIngestedRecordTime) match { | ||
| case None => | ||
| logger.warn( |
There was a problem hiding this comment.
Tests are currently hitting this line. Could it be that the verdict ingestion is ahead of the reference store ingestion so that lookupActiveOpenMiningRounds(lastIngestedRecordTime) doesn't have an answer yet?
| )(implicit tc: TraceContext): Future[Option[CantonTimestamp]] | ||
|
|
||
| /** The lowest round number that may be safe to prune | ||
| * The criterion is based on the prescence of `OpenMiningRound`, |
There was a problem hiding this comment.
| * The criterion is based on the prescence of `OpenMiningRound`, | |
| * The criterion is based on the presence of `OpenMiningRound`, |
| * lower round -- remain active. AND the verdict ingestion has moved past the | ||
| * `record_time` of OpenMiningRound's archival. | ||
| */ | ||
| class PruneRewardAccountingTrigger( |
There was a problem hiding this comment.
Can you please add some basic metrics to this trigger? Maybe a Gauge with the latest round eligible for pruning, or some meters for the values in RewardAccountingPruneSummary?
| where archived.store_id = $storeId and archived.migration_id = $migrationId | ||
| and archived.package_name = ${pqn.packageName} | ||
| and archived.template_id_qualified_name = ${pqn.qualifiedName} | ||
| and archived.round is not null |
There was a problem hiding this comment.
Can the round ever be null for OpenMingRound contracts?
There was a problem hiding this comment.
not really, I was probably trying to make use of the index, but this can be removed as we have another one to cover this.
"scan_rewards_reference_store_archived_round" btree (store_id, migration_id, round) WHERE round IS NOT NULL
"scan_rewards_reference_store_archived_temporal" btree (store_id, migration_id, package_name, template_id_qualified_name, archived_at) INCLUDE (created_at)
| * Because both are idempotent operations and re-running either for the | ||
| * same or an older round is a no-op. | ||
| * If pruning of either store fails, the task will be retried. |
There was a problem hiding this comment.
We also need to consider the case where the task fails in between the two pruning operations, and then all retries fail because of intermittent database connection issues.
In that case the task will be abandoned, and a new task eventually created. The logic for creating the new task needs to work even if exactly one of the two independent pruning transactions completed. It looks like this works because we first delete the reward accounting data, and the result of retrieveTasks only depends on the TCS store which is pruned second. I would mention this in the comment though.
| * | ||
| * Returns number of rows deleted. | ||
| */ | ||
| def pruneArchivedDataForRound( |
There was a problem hiding this comment.
| def pruneArchivedDataForRound( | |
| def pruneArchivedDataBeforeRound( |
or similar, "for" sounds like it would only delete data for that particular round
|
|
||
| import scala.concurrent.{ExecutionContext, Future} | ||
|
|
||
| /** Periodically deletes reward-accounting data that is no longer needed, one |
There was a problem hiding this comment.
Good point! I think a configurable retention window with a default of 1 week (or at least a day) would be very useful for debugging. In case of a high severity issue like a root hash mismatch we might want to inspect the state of the database, and it might take a while to get a data dump from all SVs.
| ) | ||
| ) | ||
|
|
||
| override def retrieveTasks()(implicit tc: TraceContext): Future[Seq[Task]] = |
There was a problem hiding this comment.
The logic in this method is quite complex. Can we unit test it? If you want to avoid setting up the stores with real contracts, you could factor out the implementation into an independent function and then call it with test values, like we did with AcsSnapshotTrigger.retrieveTaskForCurrentMigration.
Fixes #6136
TODO: add release notes
Pull Request Checklist
Cluster Testing
/cluster_teston this PR to request it, and ping someone with access to the DA-internal system to approve it./upgrade_teston this PR to request it, and ping someone with access to the DA-internal system to approve it./hdm_teston this PR to request it, and ping someone with access to the DA-internal system to approve it./lsu_teston this PR to request it, and ping someone with access to the DA-internal system to approve it.PR Guidelines
Fixes #n, and mention issues worked on using#nMerge Guidelines