Skip to content

Commit

Permalink
agent did api resolver cache (#855)
Browse files Browse the repository at this point in the history
In some cases, specifically when more than one identity exists, the `connect()` method can take over a minute to load.

This is because DHT resolution takes places for each identity in the system when calling `identities.list()`.

I won't go into the details of why resolution happens, but it's part of getting the signer to the DWN tenant for each identity.

This PR implements @csuwildcat's cache implementation on the `AgentDidApi` class which takes into account which DIDs are managed by the agent (or the agent's own DID). These DIDs are never evicted from the cache, only updated versions are inserted after a successful resolution occurs.

Other DIDs are evicted as usual.

In addition to this I have increased the `recordId` reference storage cache to 21 days instead of 2 hours. These values don't change, so we should cache them for as long as possible. The max items is 1,000 and it will evict items based on last seen.  21 days was chosen because the maximum allowed timeout in Node is 24 days, I reduced it as a buffer.

This should speed up `connect()` time and all signing of messages throughout the system.
  • Loading branch information
LiranCohen committed Aug 28, 2024
1 parent 7347438 commit 5ac4fe5
Show file tree
Hide file tree
Showing 16 changed files with 281 additions and 35 deletions.
9 changes: 9 additions & 0 deletions .changeset/kind-deers-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@web5/user-agent": patch
"@web5/agent": patch
"@web5/dids": patch
"@web5/identity-agent": patch
"@web5/proxy-agent": patch
---

Implement DidResolverCache thats specific to Agent usage
17 changes: 12 additions & 5 deletions .github/workflows/alpha-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ jobs:

env:
# Packages not listed here will be excluded from publishing
PACKAGES: "agent api common credentials crypto crypto-aws-kms dids identity-agent proxy-agent user-agent"
# These are currently in a specific order due to dependency requirements
PACKAGES: "crypto crypto-aws-kms common dids credentials agent identity-agent proxy-agent user-agent api"

steps:
- name: Checkout source
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1

# https://cashapp.github.io/hermit/usage/ci/
- name: Init Hermit
uses: cashapp/activate-hermit@31ce88b17a84941bb1b782f1b7b317856addf286 #v1.1.0
uses: cashapp/activate-hermit@v1
with:
cache: "true"

Expand Down Expand Up @@ -63,11 +64,17 @@ jobs:
node ./scripts/bump-workspace.mjs --prerelease=$ALPHA_PRERELEASE
shell: bash

- name: Build all workspace packages
- name: Build all workspace packages sequentially
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
run: pnpm --recursive --stream build

run: |
for package in $PACKAGES; do
cd packages/$package
pnpm build
cd ../..
done
shell: bash

- name: Publish selected @web5/* packages
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
Expand Down
3 changes: 2 additions & 1 deletion audit-ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"mysql2",
"braces",
"GHSA-rv95-896h-c2vc",
"GHSA-952p-6rrq-rcjv"
"GHSA-952p-6rrq-rcjv",
"GHSA-4vvj-4cpr-p986"
]
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"elliptic@>=4.0.0 <=6.5.6": ">=6.5.7",
"elliptic@>=2.0.0 <=6.5.6": ">=6.5.7",
"elliptic@>=5.2.1 <=6.5.6": ">=6.5.7",
"micromatch@<4.0.8": ">=4.0.8"
"micromatch@<4.0.8": ">=4.0.8",
"webpack@<5.94.0": ">=5.94.0"
}
}
}
2 changes: 1 addition & 1 deletion packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"@tbd54566975/dwn-sdk-js": "0.4.6",
"@web5/common": "1.0.0",
"@web5/crypto": "workspace:*",
"@web5/dids": "1.1.0",
"@web5/dids": "workspace:*",
"abstract-level": "1.0.4",
"ed25519-keygen": "0.4.11",
"isomorphic-ws": "^5.0.0",
Expand Down
72 changes: 72 additions & 0 deletions packages/agent/src/agent-did-resolver-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { DidResolutionResult, DidResolverCache, DidResolverCacheLevel, DidResolverCacheLevelParams } from '@web5/dids';
import { Web5PlatformAgent } from './types/agent.js';


/**
* AgentDidResolverCache keeps a stale copy of the Agent's managed Identity DIDs and only evicts and refreshes upon a successful resolution.
* This allows for quick and offline access to the internal DIDs used by the agent.
*/
export class AgentDidResolverCache extends DidResolverCacheLevel implements DidResolverCache {

/**
* Holds the instance of a `Web5PlatformAgent` that represents the current execution context for
* the `AgentDidApi`. This agent is used to interact with other Web5 agent components. It's vital
* to ensure this instance is set to correctly contextualize operations within the broader Web5
* Agent framework.
*/
private _agent?: Web5PlatformAgent;

/** A map of DIDs that are currently in-flight. This helps avoid going into an infinite loop */
private _resolving: Map<string, boolean> = new Map();

constructor({ agent, db, location, ttl }: DidResolverCacheLevelParams & { agent?: Web5PlatformAgent }) {
super ({ db, location, ttl });
this._agent = agent;
}

get agent() {
if (!this._agent) {
throw new Error('Agent not initialized');
}
return this._agent;
}

set agent(agent: Web5PlatformAgent) {
this._agent = agent;
}

/**
* Get the DID resolution result from the cache for the given DID.
*
* If the DID is managed by the agent, or is the agent's own DID, it will not evict it from the cache until a new resolution is successful.
* This is done to achieve quick and offline access to the agent's own managed DIDs.
*/
async get(did: string): Promise<DidResolutionResult | void> {
try {
const str = await this.cache.get(did);
const cachedResult = JSON.parse(str);
if (!this._resolving.has(did) && Date.now() >= cachedResult.ttlMillis) {
this._resolving.set(did, true);
if (this.agent.agentDid.uri === did || 'undefined' !== typeof await this.agent.identity.get({ didUri: did })) {
try {
const result = await this.agent.did.resolve(did);
if (!result.didResolutionMetadata.error) {
this.set(did, result);
}
} finally {
this._resolving.delete(did);
}
} else {
this._resolving.delete(did);
this.cache.nextTick(() => this.cache.del(did));
}
}
return cachedResult.value;
} catch(error: any) {
if (error.notFound) {
return;
}
throw error;
}
}
}
19 changes: 13 additions & 6 deletions packages/agent/src/did-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import type {
DidMetadata,
PortableDid,
DidMethodApi,
DidResolverCache,
DidDhtCreateOptions,
DidJwkCreateOptions,
DidResolutionResult,
DidResolutionOptions,
DidVerificationMethod,
DidResolverCache,
} from '@web5/dids';

import { BearerDid, Did, UniversalResolver } from '@web5/dids';
Expand All @@ -18,7 +18,7 @@ import type { AgentKeyManager } from './types/key-manager.js';
import type { ResponseStatus, Web5PlatformAgent } from './types/agent.js';

import { InMemoryDidStore } from './store-did.js';
import { DidResolverCacheMemory } from './prototyping/dids/resolver-cache-memory.js';
import { AgentDidResolverCache } from './agent-did-resolver-cache.js';

export enum DidInterface {
Create = 'Create',
Expand Down Expand Up @@ -87,8 +87,10 @@ export interface DidApiParams {
* An optional `DidResolverCache` instance used for caching resolved DID documents.
*
* Providing a cache implementation can significantly enhance resolution performance by avoiding
* redundant resolutions for previously resolved DIDs. If omitted, a no-operation cache is used,
* which effectively disables caching.
* redundant resolutions for previously resolved DIDs. If omitted, the default is an instance of `AgentDidResolverCache`.
*
* `AgentDidResolverCache` keeps a stale copy of the Agent's managed Identity DIDs and only refreshes upon a successful resolution.
* This allows for quick and offline access to the internal DIDs used by the agent.
*/
resolverCache?: DidResolverCache;

Expand Down Expand Up @@ -120,10 +122,10 @@ export class AgentDidApi<TKeyManager extends AgentKeyManager = AgentKeyManager>
}

// Initialize the DID resolver with the given DID methods and resolver cache, or use a default
// in-memory cache if none is provided.
// AgentDidResolverCache if none is provided.
super({
didResolvers : didMethods,
cache : resolverCache ?? new DidResolverCacheMemory()
cache : resolverCache ?? new AgentDidResolverCache({ agent, location: 'DATA/AGENT/DID_CACHE' })
});

this._agent = agent;
Expand Down Expand Up @@ -152,6 +154,11 @@ export class AgentDidApi<TKeyManager extends AgentKeyManager = AgentKeyManager>

set agent(agent: Web5PlatformAgent) {
this._agent = agent;

// AgentDidResolverCache should set the agent if it is the type of cache being used
if ('agent' in this.cache) {
this.cache.agent = agent;
}
}

public async create({
Expand Down
13 changes: 6 additions & 7 deletions packages/agent/src/identity-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,13 @@ export class AgentIdentityApi<TKeyManager extends AgentKeyManager = AgentKeyMana
// Retrieve the list of Identities from the Agent's Identity store.
const storedIdentities = await this._store.list({ agent: this.agent, tenant });

const identities: BearerIdentity[] = [];
const identities = await Promise.all(
storedIdentities.map(async metadata => {
return this.get({ didUri: metadata.uri, tenant: metadata.tenant });
})
);

for (const metadata of storedIdentities) {
const identity = await this.get({ didUri: metadata.uri, tenant: metadata.tenant });
identities.push(identity!);
}

return identities;
return identities.filter(identity => typeof identity !== 'undefined') as BearerIdentity[];
}

public async manage({ portableIdentity }: {
Expand Down
1 change: 1 addition & 0 deletions packages/agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type * from './types/permissions.js';
export type * from './types/sync.js';
export type * from './types/vc.js';

export * from './agent-did-resolver-cache.js';
export * from './bearer-identity.js';
export * from './cached-permissions.js';
export * from './crypto-api.js';
Expand Down
10 changes: 7 additions & 3 deletions packages/agent/src/store-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,20 @@ export class DwnDataStore<TStoreObject extends Record<string, any> = Jwk> implem

/**
* Index for mappings from Store Identifier to DWN record ID.
* Since these values don't change, we can use a long TTL.
*
* Up to 1,000 entries are retained for 2 hours.
* Up to 1,000 entries are retained for 21 days.
* NOTE: The maximum number for the ttl is 2^31 - 1 milliseconds (24.8 days), setting to 21 days to be safe.
*/
protected _index = new TtlCache<string, string>({ ttl: ms('2 hours'), max: 1000 });
protected _index = new TtlCache<string, string>({ ttl: ms('21 days'), max: 1000 });

/**
* Cache of tenant DIDs that have been initialized with the protocol.
* This is used to avoid redundant protocol initialization requests.
*
* Since these are default protocols and unlikely to change, we can use a long TTL.
*/
protected _protocolInitializedCache: TtlCache<string, boolean> = new TtlCache({ ttl: ms('1 hour'), max: 1000 });
protected _protocolInitializedCache: TtlCache<string, boolean> = new TtlCache({ ttl: ms('21 days'), max: 1000 });

/**
* The protocol assigned to this storage instance.
Expand Down
5 changes: 3 additions & 2 deletions packages/agent/src/test-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import type { AbstractLevel } from 'abstract-level';
import { Level } from 'level';
import { LevelStore, MemoryStore } from '@web5/common';
import { DataStoreLevel, Dwn, EventEmitterStream, EventLogLevel, MessageStoreLevel, ResumableTaskStoreLevel } from '@tbd54566975/dwn-sdk-js';
import { DidDht, DidJwk, DidResolutionResult, DidResolverCache, DidResolverCacheLevel } from '@web5/dids';
import { DidDht, DidJwk, DidResolutionResult, DidResolverCache } from '@web5/dids';

import type { Web5PlatformAgent } from './types/agent.js';

import { AgentDidApi } from './did-api.js';
import { AgentDidResolverCache } from './agent-did-resolver-cache.js';
import { AgentDwnApi } from './dwn-api.js';
import { AgentSyncApi } from './sync-api.js';
import { Web5RpcClient } from './rpc-client.js';
Expand Down Expand Up @@ -287,7 +288,7 @@ export class PlatformAgentTestHarness {
const { didStore, identityStore, keyStore } = stores;

// Setup DID Resolver Cache
const didResolverCache = new DidResolverCacheLevel({
const didResolverCache = new AgentDidResolverCache({
location: testDataPath('DID_RESOLVERCACHE')
});

Expand Down
Loading

0 comments on commit 5ac4fe5

Please sign in to comment.