Skip to content

CIP-104: Add temporal ACS store for doing asOf lookups#4126

Closed
dfordivam wants to merge 26 commits intodfordivam/feat-cip-104-activity-recordsfrom
dfordivam/cip-104-scan-tcs-store
Closed

CIP-104: Add temporal ACS store for doing asOf lookups#4126
dfordivam wants to merge 26 commits intodfordivam/feat-cip-104-activity-recordsfrom
dfordivam/cip-104-scan-tcs-store

Conversation

@dfordivam
Copy link
Contributor

Fixes #4118

Note this does not implement caching, only the DB based queries

Pull Request Checklist

Cluster Testing

  • If a cluster test is required, comment /cluster_test on this PR to request it, and ping someone with access to the DA-internal system to approve it.
  • If a hard-migration test is required (from the latest release), comment /hdm_test on this PR to request it, and ping someone with access to the DA-internal system to approve it.

PR Guidelines

  • Include any change that might be observable by our partners or affect their deployment in the release notes.
  • Specify fixed issues with Fixes #n, and mention issues worked on using #n
  • Include a screenshot for frontend-related PRs - see README or use your favorite screenshot tool

Merge Guidelines

  • Make the git commit message look sensible when squash-merging on GitHub (most likely: just copy your PR description).

@dfordivam dfordivam self-assigned this Feb 25, 2026
@dfordivam dfordivam requested a review from meiersi-da February 25, 2026 09:59
@dfordivam
Copy link
Contributor Author

@meiersi-da This is the basic implementation of ScanTcsStore for a quick round of review.

Copy link
Contributor

@meiersi-da meiersi-da left a comment

Choose a reason for hiding this comment

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

Nice work!

);

create index acs_store_archived_template_temporal
on acs_store_archived_template (store_id, migration_id, template_id_qualified_name, created_at, archived_at);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
on acs_store_archived_template (store_id, migration_id, template_id_qualified_name, created_at, archived_at);
on acs_store_archived_template (store_id, migration_id, template_id_qualified_name, created_at) include (archived_at);

probably the better option to have cheaper internal btree nodes and statistics

Copy link
Contributor

Choose a reason for hiding this comment

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

actually I just realized that we will typically be looking up recent timestamps, which means changing the index to efficiently skip over past events is valuable.

Concretely

Suggested change
on acs_store_archived_template (store_id, migration_id, template_id_qualified_name, created_at, archived_at);
on acs_store_archived_template (store_id, migration_id, template_id_qualified_name, archived_at) include (created_at);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


-- lookup FeaturedAppRight
create index scan_tcs_store_active_sid_mid_tid_farp
on scan_tcs_store_active (store_id, migration_id, template_id_qualified_name, featured_app_right_provider)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
on scan_tcs_store_active (store_id, migration_id, template_id_qualified_name, featured_app_right_provider)
on scan_tcs_store_active (store_id, migration_id, featured_app_right_provider)

seems enough or not?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will investigate

Copy link
Contributor

Choose a reason for hiding this comment

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

also make the index partial to exclude NULLs. That ensures a small index size.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I got rid of this as discussed yesterday we won't be doing lookup on party IDs


-- record_time of the transaction that archived the contract, in micros since epoch.
archived_at bigint not null
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this given that we use like <actual_store_table_active> below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is used only for the unit test of DbTemporalAcsStoreTest

Copy link
Contributor

@meiersi-da meiersi-da Feb 26, 2026

Choose a reason for hiding this comment

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

Consider renaming it to test_ instead of template_ then.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


-- temporal query support: created_at + archived_at filtering for point-in-time lookups on the archive table
create index scan_tcs_store_archived_temporal
on scan_tcs_store_archived (store_id, migration_id, template_id_qualified_name, created_at, archived_at);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
on scan_tcs_store_archived (store_id, migration_id, template_id_qualified_name, created_at, archived_at);
on scan_tcs_store_archived (store_id, migration_id, template_id_qualified_name, created_at) include (archived_at);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, ie using archived_at in index

)
.asRuntimeException()
}
.flatMap(_ => waitUntilRecordTimeReached(asOf))
Copy link
Contributor

Choose a reason for hiding this comment

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

@nicu-da : I remember you fixing some memory leak problems wrt this kind of recursive loop on futures. Is this construction here safe?

Copy link
Contributor

Choose a reason for hiding this comment

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

After the fix in https://github.com/DACH-NY/canton-network-node/pull/9212 it should be safe. Modern futures are also pretty safe in chaining this way as they will flatten the recursion loop but it can still cause issues. Main issue if the waitUntilRecordTimeReached is called from a "hotspot" in the ingestion pipeline with a asOf which is relatively far in the future, thus we will have large number of waitUntilRecordTimeReached running in loops for each ingestion from the looks of it.
Ideally we should reverse and just add a promise that gets fulfilled when the ingestion reaches the asOf timestamp, removing the constant looping.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the moment the temporal store APIs have no external endpoints, and a single automation doing lookups, hence I went with the simpler approach. I doubt we will ever be exposing this to an external endpoint. The lookup record_time also would typically not be far in future. So I will let this be as it is for now.

Copy link
Contributor

Choose a reason for hiding this comment

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

The lookup record_time also would typically not be far in future. So I will let this be as it is for now.

This is likely true. It is though one of these things that's going to be expensive to rediscover as part of scalability work. Let's create at least a tech-debt item the Scalability milestone to switch to a promise-based implementation.

Copy link
Contributor

Choose a reason for hiding this comment

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

The situation where it may not be true are catch-up situations. The annoying thing is that these are exceptional, but operationally critical circumstances. So keeping them in mind during development is valuable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I fixed this to work similar to how offset based wakeup of Futures is currently being done.

import slick.jdbc.canton.SQLActionBuilder
import slick.sql.SqlStreamingAction

trait TemporalAcsQueries extends AcsQueries {
Copy link
Contributor

Choose a reason for hiding this comment

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

Side note: @adetokunbo -- renaming DbAppActiviyRecordStore to AppActivityRecordQueries would seem to be more clear, as it really is mostly a helper class to build the queries akin to the TemporalAcsQueries here.

Food for thought. Not key to change, but might help with code clarity.

protected def c(i: Int): Contract[AppRewardCoupon.ContractId, AppRewardCoupon] =
appRewardCoupon(i, dsoParty, contractId = validContractId(i))

"DbTemporalAcsStore" should {
Copy link
Contributor

Choose a reason for hiding this comment

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

missing test for waitUntilRecordTimeReached

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added


import scala.concurrent.{ExecutionContext, Future}

class DbScanTemporalAcsStore(
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider using the Tcs as an abbreviation for TemporalContractStore analogous to Acs for ActiveContractStore.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

): Future[Seq[ContractWithState[TCid, T]]] =
multiDomainAcsStore.listContractsAsOf(companion, asOf, limit)

def lookupFeaturedAppRightsAsOf(
Copy link
Contributor

Choose a reason for hiding this comment

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

we're missing the one for looking them up by party. Ideally as a batch call.

Copy link
Contributor

Choose a reason for hiding this comment

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

and in a later PR with caching.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmmm. did we need [Party] based lookup in our flow?

Copy link
Contributor

Choose a reason for hiding this comment

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

We'll need the featured app status for all confirmers in the verdict of a confirmation request. My intuition was that organizing the code such that we pre-resolve all of them in a single call is not too onerous and has better performance properties.

Copy link
Contributor

Choose a reason for hiding this comment

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

Happy to postpone that though as a performance optimization, so we get to a working implementation more quickly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As discussed yesterday on call, we plan on doing a single AsOf lookup of all parties per round.

import org.lfdecentralizedtrust.splice.store.{AppStore, MultiDomainAcsStore}
import org.lfdecentralizedtrust.splice.store.db.AcsInterfaceViewRowData

trait ScanTemporalAcsStore extends AppStore {
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider repeating the comment wrt purpose here from design doc. Goal is to inform future readers as to the tight scoping of this. Perhaps a name that relates to that would help ScanRewardsReferenceDataStore might be an option.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added

Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
Signed-off-by: Divam <dfordivam@gmail.com>
@dfordivam dfordivam force-pushed the dfordivam/cip-104-scan-tcs-store branch from cb68841 to e0e833f Compare February 27, 2026 08:18
Copy link
Contributor Author

@dfordivam dfordivam left a comment

Choose a reason for hiding this comment

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

@meiersi-da I have added the fixes and pushed the branch over latest main. PTAL


-- record_time of the transaction that archived the contract, in micros since epoch.
archived_at bigint not null
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

);

create index acs_store_archived_template_temporal
on acs_store_archived_template (store_id, migration_id, template_id_qualified_name, created_at, archived_at);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


-- lookup FeaturedAppRight
create index scan_tcs_store_active_sid_mid_tid_farp
on scan_tcs_store_active (store_id, migration_id, template_id_qualified_name, featured_app_right_provider)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I got rid of this as discussed yesterday we won't be doing lookup on party IDs


-- temporal query support: created_at + archived_at filtering for point-in-time lookups on the archive table
create index scan_tcs_store_archived_temporal
on scan_tcs_store_archived (store_id, migration_id, template_id_qualified_name, created_at, archived_at);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, ie using archived_at in index

)
.asRuntimeException()
}
.flatMap(_ => waitUntilRecordTimeReached(asOf))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I fixed this to work similar to how offset based wakeup of Futures is currently being done.

}
}

override def ingestUpdateBatch(batch: NonEmptyList[TreeUpdateOrOffsetCheckpoint])(implicit
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually I missed it. Added it now, along with a test.

protected def c(i: Int): Contract[AppRewardCoupon.ContractId, AppRewardCoupon] =
appRewardCoupon(i, dsoParty, contractId = validContractId(i))

"DbTemporalAcsStore" should {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added


import scala.concurrent.{ExecutionContext, Future}

class DbScanTemporalAcsStore(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

): Future[Seq[ContractWithState[TCid, T]]] =
multiDomainAcsStore.listContractsAsOf(companion, asOf, limit)

def lookupFeaturedAppRightsAsOf(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As discussed yesterday on call, we plan on doing a single AsOf lookup of all parties per round.

import org.lfdecentralizedtrust.splice.store.{AppStore, MultiDomainAcsStore}
import org.lfdecentralizedtrust.splice.store.db.AcsInterfaceViewRowData

trait ScanTemporalAcsStore extends AppStore {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added

@dfordivam dfordivam changed the base branch from main to dfordivam/feat-cip-104-activity-records February 27, 2026 08:28
@dfordivam dfordivam deleted the branch dfordivam/feat-cip-104-activity-records March 5, 2026 06:01
@dfordivam dfordivam closed this Mar 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scan-app: Add temporal ACS store for doing asOf lookups

3 participants