Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/common/src/main/openapi/common-internal.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ components:
type: integer
description: |
Cursor for pagination. When requesting the next page of results, pass the `next_page_token` from the previous response.
Results are ordered by ledger record time, descending.

ListDsoRulesVoteResultsResponse:
type: object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ object SqlIndexInitializationTrigger {
}

/** Indexes managed by this trigger class */
val defaultIndexActions: List[IndexAction] = List(
val defaultIndexActions: List[IndexAction] = List[IndexAction](
IndexAction
.Create(
indexName = "updt_hist_crea_hi_mi_ci_import_updates",
Expand All @@ -248,13 +248,16 @@ object SqlIndexInitializationTrigger {
),
IndexAction
.Create(
indexName = "scan_txlog_store_sid_en_vot",
indexName = "scan_txlog_store_sid_rt_en_vot",
createAction = sqlu"""
create index concurrently if not exists scan_txlog_store_sid_en_vot
on scan_txlog_store (store_id, entry_number desc)
create index concurrently if not exists scan_txlog_store_sid_rt_en_vot

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@rautenrieth-da could you take a look if that's a correct way to remove unused indices? We didn't use IndexAction.Drop before but AFAIU that's the way

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.

The code looks good to me. Since this is the first time we're using it, we should check if it really works as intended. You won't trigger the code path in integration tests, you need to first deploy an old version of the app and then a version that has your PR merged. At the very least, check logs of CILR after the changes from this PR have been deployed there.

on scan_txlog_store (store_id, record_time desc, entry_number desc)
where entry_type = 'vot'
""",
),
// Superseded by scan_txlog_store_sid_rt_en_vot when vote results switched from
// entry_number to record time ordering.
IndexAction.Drop(indexName = "scan_txlog_store_sid_en_vot"),
)

sealed trait Task extends Product with Serializable with PrettyPrinting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ trait DbVotesTxLogStoreQueryBuilder[TXE]
TxLogQueries.SelectFromTxLogTableResult
], TxLogQueries.SelectFromTxLogTableResult, Effect.Read] = {
val afterCondition = after match {
case Some(a) => Some(sql"""entry_number < $a""")
case Some(a) =>
Some(
sql"""(record_time, entry_number) < ((select record_time from #$txLogTableName where store_id = $txLogStoreId and entry_number = $a), $a)"""

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.

did you check the plane for this? the SQL query looks weird to me.

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.

+1 to commenting with the query plan. We just dropped the index on (store_id, entry_number), how can it quickly find the record time from the entry number?

I would have expected that we do either:

  • Sort lexicographically by (record_time, entry_number), and include the record time in the API pagination token (along with the entry number)
  • Argue why two vote requests can never have the same record time, sort by record time only, and paginate by record time (instead of entry number). If you store the record time as "micros since epoch", you don't even need to change the type of pageToken in the API.

@ray-roestenburg-da ray-roestenburg-da Jun 15, 2026

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.

We discussed this exactly (out of band) and decided we should use the (record_time, entry_number) in page token

)
case None => None
}
val actionNameCondition = actionName match {
Expand Down Expand Up @@ -92,7 +95,7 @@ trait DbVotesTxLogStoreQueryBuilder[TXE]
txLogTableName,
txLogStoreId,
where = whereClause.toActionBuilder,
orderLimit = sql"""order by entry_number desc limit ${sqlLimit(limit)}""",
orderLimit = sql"""order by record_time desc, entry_number desc limit ${sqlLimit(limit)}""",
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ class SqlIndexInitializationTriggerStoreTest
indexNames should contain allElementsOf Seq(
"updt_hist_crea_hi_mi_ci_import_updates",
"updt_hist_tran_hi_eth",
"scan_txlog_store_sid_en_vot",
"scan_txlog_store_sid_rt_en_vot",
)
indexNames should not contain "scan_txlog_store_sid_en_vot"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,66 @@ abstract class ScanStoreTest
val voteRequestContracts = mkVoteRequests()
assertListOfAllPastVoteRequestResults(voteRequestContracts, store)
}

"return results in record time order, including backfilled entries" in {
// Simulates a txlog where backfilling inserted older entries after live ingestion
// started: backfilling walks backwards in time, so backfilled entries get entry
// numbers in reverse record time order.
val baseTime = Instant.now().truncatedTo(ChronoUnit.MICROS)
def recordTime(n: Int) = baseTime.plusSeconds(n.toLong)
val voteRequests = (1 to 4).map { n =>
voteRequest(
requester = userParty(n),
votes = Seq(
new Vote(userParty(n).toProtoPrimitive, true, new Reason("", ""), Optional.empty())
),
)
}
val results = voteRequests.map(mkVoteRequestResult(_))
def closeVoteRequest(store: ScanStore, n: Int) =
dummyDomain.exercise(
contract = dsoRules(dsoParty),
interfaceId = Some(DsoRules.TEMPLATE_ID_WITH_PACKAGE_ID),
choiceName = DsoRulesCloseVoteRequest.choice.name,
choiceArgument = mkCloseVoteRequest(voteRequests(n - 1).contractId),
exerciseResult = results(n - 1).toValue,
recordTime = recordTime(n),
)(store.multiDomainAcsStore)
for {
store <- mkStore()
_ <- MonadUtil.sequentialTraverse(voteRequests)(
dummyDomain.create(_)(store.multiDomainAcsStore)
)
// Live ingestion of results 3 and 4...
_ <- closeVoteRequest(store, 3)
_ <- closeVoteRequest(store, 4)
// ...then backfilling inserts the older results, walking backwards in time.
_ <- closeVoteRequest(store, 2)
_ <- closeVoteRequest(store, 1)
page1 <- store.listVoteRequestResults(
None,
None,
None,
None,
None,
PageLimit.tryCreate(3),
)
page2 <- store.listVoteRequestResults(
None,
None,
None,
None,
None,
PageLimit.tryCreate(3),
page1.nextPageToken,
)
} yield {
page1.resultsInPage.map(_.request.requester) shouldBe
Seq(4, 3, 2).map(userParty(_).toProtoPrimitive)
page2.resultsInPage.map(_.request.requester) shouldBe
Seq(1).map(userParty(_).toProtoPrimitive)
}
}
}

"listVoteRequestsByTrackingCid" should {
Expand Down
1 change: 1 addition & 0 deletions apps/sv/frontend/src/routes/governance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export const Governance: React.FC = () => {
uniqueId="vote-history"
showStatus
showVoteStats
sortOrder="effectiveAtDesc"
fetchNextPage={voteResultsInfiniteQuery.fetchNextPage}
hasNextPage={voteResultsInfiniteQuery.hasNextPage}
isFetchingNextPage={voteResultsInfiniteQuery.isFetchingNextPage}
Expand Down
Loading