Skip to content

Conversation

@JGiter
Copy link
Contributor

@JGiter JGiter commented Nov 6, 2024

  • Documents will be synchronized starting from the block of latest synchronization
  • Only documents updated since the latest synchronization will be synchronized
  • Fixed using of DID synchronization job

Tested manually, because tests scenarios require application restart from desired state, which is not supported in Nest.js.
Test scenarios:

  1. Clean start without cached documents. Events are fetch from block 0
debug [DIDService] : 2025-05-22T07:42:52.199Z - Fetched 0 DID events from interval [0, 1000000]
debug [DIDService] : 2025-05-22T07:42:52.806Z - Fetched 0 DID events from interval [1000000, 2000000]
...
debug [DIDService] : 2025-05-22T07:46:39.332Z - Fetched 526 DID events from interval [32000000, 32055382]
debug [DIDService] : 2025-05-22T07:46:39.332Z - Update document events count 48716
debug [DIDService] : 2025-05-22T07:46:39.351Z - Fetched DID update events from block 0 to block 32055382
debug [DIDService] : 2025-05-22T07:46:39.408Z - Marked 0 stale documents
debug [DIDService] : 2025-05-22T07:46:39.423Z - Synchronizing 0 documents

2 Restart after clean start. Events are fetched from last sync block

debug [DIDService] : 2025-05-22T07:55:28.524Z - Fetched 0 DID events from interval [32055382, 32055467]
debug [DIDService] : 2025-05-22T07:55:28.526Z - Update document events count 0
debug [DIDService] : 2025-05-22T07:55:28.526Z - Fetched DID update events from block 32055382 to block 32055467
debug [DIDService] : 2025-05-22T07:55:28.548Z - Synchronizing 0 documents
  1. Add new document in cache.
debug [DIDService] : 2025-05-22T07:57:57.516Z - Add cached document for did: did:ethr:volta:0xc56***
debug [IPFSService] : 2025-05-22T07:58:33.755Z - trying to get Qme5jE7wcWw2hH68gPykR7tWmyS1gZwJaXAyn9FEeL9WDW
debug [IPFSService] : 2025-05-22T07:58:33.756Z - trying to get QmPdna1nbHSd3BLUnDCz6Vikqo9J7o9wXDKhVU7n2vseNp
...
debug [IPFSService] : 2025-05-22T07:58:33.779Z - trying to get QmQtbQiTnnJ5cMidfxjTwKE4QQpCkM3cYM7yGj4YMpqM1X
debug [IPFSService] : 2025-05-22T07:58:34.457Z - got QmNffpFGhovF1UYY3YPfLzjR2atpY45sGfXnd28RrvfJmC
...
debug [IPFSService] : 2025-05-22T07:58:34.469Z - got QmePLQtz5KVYfKR1s8FMK4dR7fudWZTnP9henaHDQ1Y8gr
debug [PinProcessor] : 2025-05-22T07:58:34.471Z - Waiting 1
...
debug [PinProcessor] : 2025-05-22T07:58:34.471Z - Waiting 7
...
debug [IPFSService] : 2025-05-22T07:58:38.774Z - Claim is not resolved in IPFS. Claim CID Qmd1rKLzkjggHPTcYgt3g1nc4mykzcNprvP182Niiag47j
...
debug [IPFSService] : 2025-05-22T07:58:38.780Z - Claim is not resolved in IPFS. Claim CID QmQtbQiTnnJ5cMidfxjTwKE4QQpCkM3cYM7yGj4YMpqM1X
debug [DIDService] : 2025-05-22T07:58:38.806Z - Document did:ethr:volta:0xc56*** was synchronized
info [DIDController] : 2025-05-22T07:58:38.807Z - Retrieved document for did: did:ethr:volta:0xc56***
  1. Full synchronization with one document in cache. All document events are retrieved and document synchronized
debug [DIDService] : 2025-05-22T08:11:54.649Z - Fetched 0 DID events from interval [0, 1000000]
...
debug [DIDService] : 2025-05-22T08:15:58.688Z - Fetched 526 DID events from interval [32000000, 32055579]
debug [DIDService] : 2025-05-22T08:15:58.688Z - Update document events count 48716
debug [DIDService] : 2025-05-22T08:15:58.707Z - Fetched DID update events from block 0 to block 32055579
debug [DIDService] : 2025-05-22T08:15:58.773Z - Marked 1 stale documents
debug [DIDService] : 2025-05-22T08:15:58.785Z - Synchronizing 1 documents
debug [DIDService] : 2025-05-22T08:15:58.785Z - Synchronizing DID did:ethr:volta:0xc56***
info [DIDService] : 2025-05-22T08:15:58.787Z - Refreshing cached document for did: did:ethr:volta:0xc56***
debug [DIDProcessor] : 2025-05-22T08:15:58.788Z - Starting refreshing document did:ethr:volta:0xc56***
debug [DIDProcessor] : 2025-05-22T08:15:58.789Z - Waiting job did:ethr:volta:0xc56***
debug [DIDService] : 2025-05-22T08:16:00.012Z - Document did:ethr:volta:0xc56*** was synchronized
debug [DIDProcessor] : 2025-05-22T08:16:00.012Z - IPFS cluster pinning disabled. Skipping for did:ethr:volta:0xc56***
  1. Publish two claims and restart. Fetches new events and synchronize
debug [DIDService] : 2025-05-22T08:31:09.558Z - Fetched 2 DID events from interval [32055579, 32055709]
debug [DIDService] : 2025-05-22T08:31:09.581Z - Update document events count 2
debug [DIDService] : 2025-05-22T08:31:09.581Z - Fetched DID update events from block 32055579 to block 32055709
includeAllRoles: false, verifying only accepted roles
DID Login Strategy is now logged into cache server after 1888ms
debug [DIDService] : 2025-05-22T08:31:09.615Z - Marked 1 stale documents
debug [DIDService] : 2025-05-22T08:31:09.626Z - Synchronizing 1 documents
debug [DIDService] : 2025-05-22T08:31:09.626Z - Synchronizing DID did:ethr:volta:0xc56***
debug [DIDProcessor] : 2025-05-22T08:31:09.628Z - Waiting job did:ethr:volta:0xc56***
info [DIDService] : 2025-05-22T08:31:09.629Z - Refreshing cached document for did: did:ethr:volta:0xc56***
debug [DIDProcessor] : 2025-05-22T08:31:09.630Z - Starting refreshing document did:ethr:volta:0xc56***
debug [IPFSService] : 2025-05-22T08:31:11.346Z - trying to get QmPxFs16VdEweo6tCeRUSzCTwmu44ZfJZCMMRtfGCwL5rZ
debug [IPFSService] : 2025-05-22T08:31:11.879Z - got QmPxFs16VdEweo6tCeRUSzCTwmu44ZfJZCMMRtfGCwL5rZ
debug [PinProcessor] : 2025-05-22T08:31:11.882Z - Waiting 29
debug [DIDService] : 2025-05-22T08:31:11.899Z - Document did:ethr:volta:0xc56*** was synchronized

@OnQueueWaiting()
async OnQueueWaiting(job: Job) {
this.logger.debug(`Waiting ${job.name} document ${job.data}`);
async OnQueueWaiting(jobId: number) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Argument to this hook differs from others https://docs.nestjs.com/techniques/queues#event-listeners-1

Copy link
Contributor

@jrhender jrhender left a comment

Choose a reason for hiding this comment

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

Good start to the PR but I think the issue with the jobs disappearing should be addressed

Comment on lines 380 to 389
).filter((doc) => {
const identity = doc.id.split(':')[3];
return changedIdentities.includes(identity);
});
didsToSynchronize.forEach(async (did) => {
this.logger.debug(`Synchronizing DID ${did.id}`);
await this.pinDocument(did.id);
});

await this.latestDidSyncRepository.save({ block: topBlock });
Copy link
Contributor

Choose a reason for hiding this comment

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

Hey @JGiter, thanks for the PR. Really exciting that this code is being improved.

I think this approach is flawed however. The issue that I see is that jobs are persisted in separate storage (Redis) and then processed asynchronously, but the latestDidSync topBlock is persisted synchronously. So we could have a situation like this:

  1. A bunch of DID update jobs are queued and lastestDidSync.block is updated
  2. The Redis storage fails or is wiped (it's in-memory, so definitely could happen) -> then the system won't update the DIDs in the lost jobs because lastestDidSync.block already ahead of these events.

I'm thinking an alternative approach to get around this problem would be to:

  1. Mark updated DIDs as "stale":
  2. Query all of the "changedIdentities" since the last check (as you've done in this PR)
  3. Mark all of these DIDs as "invalid" (in the Postgres Entities). I think something like this could be used to bulk update entities.
  4. Update latestDidSync.block
  5. Query all of DID entities with invalid status and add a job to the queue (if it doesn't exist already)
  6. When done processing the job, update the DID to be "valid"

In this above, even if the job queue is reset/wiped, the DID entities will still have invalid status, so job for them can be re-added.

Also, we can change the GET Did Doc endpoint to synchronously query the RPC if the cached DID is invalid.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. This is really good improvement

private async pinDocument(did: string): Promise<void> {
try {
await this.didQueue.add(UPDATE_DID_DOC_JOB_NAME, did);
await this.didQueue.add(UPDATE_DID_DOC_JOB_NAME, { did });
Copy link
Contributor

Choose a reason for hiding this comment

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

@JGiter does this change relate to this comment you made in the PR description?

Fixed using of DID synchronization job

Does the data need to be in an object?

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 can not say why, but when passing job as a string, I observed multiple errors error parsing JSON though there are no seemingly JSON parsing in DidProcessor. So I decided to make job as an object, same as in PinProcessor.

@PrimaryGeneratedColumn()
id: number;

@OneToOne(() => DIDDocumentEntity, (document) => document.id)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addinig relation only on DidSyncStatusEntity side. The DidDocumentEntity will not be changed

@JGiter JGiter requested a review from jrhender November 8, 2024 10:00
Copy link
Contributor

@jrhender jrhender left a comment

Choose a reason for hiding this comment

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

Some further comments but I think it is continuing to move in a good direction 👍

Comment on lines 17 to 19
@OneToOne(() => DIDDocumentEntity, (document) => document.id)
@JoinColumn()
document: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems wrong to me that this property is string type. I think that typically in a one-to-one join column, the join property has the same type as the joined entity.
See the Profile example at the start of the TypeORM documentation: https://orkhan.gitbook.io/typeorm/docs/one-to-one-relations

Suggested change
@OneToOne(() => DIDDocumentEntity, (document) => document.id)
@JoinColumn()
document: string;
@OneToOne(() => DIDDocumentEntity, (document) => document.id)
@JoinColumn()
document: DIDDocumentEntity;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I overlooked that

Comment on lines 228 to 232
const updated = await this.didRepository.save(updatedEntity);
await this.didSyncStatusRepository.save({
document: did,
status: DidSyncStatus.Synced,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be in a transaction as I think an invariant should be that there is always a didSyncStatus entity if there is a did entity.

Comment on lines 571 to 583
const staleDIDs = (
await this.didRepository.find({ select: ['id'] })
).filter((doc) => {
const identity = addressOf(doc.id);
return changedIdentities.includes(identity);
});
await this.didSyncStatusRepository
.createQueryBuilder()
.useTransaction(true)
.update(DidSyncStatusEntity)
.set({ status: DidSyncStatus.Stale })
.where({ document: In(staleDIDs) })
.execute();
Copy link
Contributor

Choose a reason for hiding this comment

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

I seems to me like a better approach would be to rely on the database to do the filtering in the where clause, rather than reading all dids into application memory and then filtering in the application. I think that, if the number of DIDs became large enough, this this.didRepository.find({ select: ['id'] query would probably fail.

I suppose the challenge might be how to use the addresses returned from changedIdentities to find the didSyncStatus entities to update. I think a couple options are:

  1. Store the address of the identity on the DidSyncStatusEntity
  2. Join to DIDDocumentEntity in the query and filter on the id property. Of course the addresses would need to be transformed to did:ethr DID ids.

Note that I think that option 2 might be slightly less complicated if we got rid of the one-to-one join entity and just added a new column to the DidDocumentEntity.
I think we only really need a single isStale boolean column (or could also keep the enum), so the impact on the performance of the database query when reading the DID Document would be negligible, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for suggestions. Option 2 seems more safe to me, because it excludes case when two status entities connected to the same document. I would also like to avoid changing document entity. It is used everywhere in ssi-hub and I am bit afraid to cause some unexpected changes. And besides property isState is not inherently part of the document.

Comment on lines 577 to 583
await this.didSyncStatusRepository
.createQueryBuilder()
.useTransaction(true)
.update(DidSyncStatusEntity)
.set({ status: DidSyncStatus.Stale })
.where({ document: In(staleDIDs) })
.execute();
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this query work for existing cached documents (already existing in the cache I mean)? Maybe I missed it, but I don't see a migration that adds a DidSyncStatusEntity for all existing cached DID Documents.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I missed that

@JGiter JGiter force-pushed the feat/incremental-document-sync branch from 5f9b0cd to 4177c78 Compare May 26, 2025 06:45
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.

3 participants