Skip to content

Commit

Permalink
Added index to SQL TTL cache (#142)
Browse files Browse the repository at this point in the history
1. Added index to SQL TTL cache by leveraging latest `dwn-sql-store`
1. Added handling of an edge race condition.

---------

Co-authored-by: Liran Cohen <c.liran.c@gmail.com>
  • Loading branch information
thehenrytsai and LiranCohen authored Jul 3, 2024
1 parent 199355d commit 704cbb4
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 25 deletions.
119 changes: 112 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@web5/dwn-server",
"type": "module",
"version": "0.4.0",
"version": "0.4.1",
"files": [
"dist",
"src"
Expand All @@ -27,7 +27,7 @@
},
"dependencies": {
"@tbd54566975/dwn-sdk-js": "0.4.0",
"@tbd54566975/dwn-sql-store": "0.6.0",
"@tbd54566975/dwn-sql-store": "0.6.1",
"better-sqlite3": "^8.5.0",
"body-parser": "^1.20.2",
"bytes": "3.1.2",
Expand Down
32 changes: 23 additions & 9 deletions src/web5-connect/sql-ttl-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ export class SqlTtlCache {
private static readonly cacheTableName = 'cacheEntries';
private static readonly cleanupIntervalInSeconds = 60;

private sqlDialect: Dialect;
private db: Kysely<CacheDatabase>;
private cleanupTimer: NodeJS.Timeout;

private constructor(sqlDialect: Dialect) {
this.db = new Kysely<CacheDatabase>({ dialect: sqlDialect });
this.sqlDialect = sqlDialect;
}

/**
Expand All @@ -27,14 +29,26 @@ export class SqlTtlCache {
}

private async initialize(): Promise<void> {
await this.db.schema
.createTable(SqlTtlCache.cacheTableName)
.ifNotExists()
// 512 chars to accommodate potentially large `state` in Web5 Connect flow
.addColumn('key', 'varchar(512)', (column) => column.primaryKey())
.addColumn('value', 'text', (column) => column.notNull())
.addColumn('expiry', 'integer', (column) => column.notNull())
.execute();

// create table if it doesn't exist
const tableExists = await this.sqlDialect.hasTable(this.db, SqlTtlCache.cacheTableName);
if (!tableExists) {
await this.db.schema
.createTable(SqlTtlCache.cacheTableName)
.ifNotExists() // kept to show supported by all dialects in contrast to ifNotExists() below, though not needed due to tableExists check above
// 512 chars to accommodate potentially large `state` in Web5 Connect flow
.addColumn('key', 'varchar(512)', (column) => column.primaryKey())
.addColumn('value', 'text', (column) => column.notNull())
.addColumn('expiry', 'integer', (column) => column.notNull())
.execute();

await this.db.schema
.createIndex('index_expiry')
// .ifNotExists() // intentionally kept commented out code to show that it is not supported by all dialects (ie. MySQL)
.on(SqlTtlCache.cacheTableName)
.column('expiry')
.execute();
}

// Start the cleanup timer
this.startCleanupTimer();
Expand Down Expand Up @@ -102,7 +116,7 @@ export class SqlTtlCache {
}

/**
* Periodically clean up expired cache entries.
* Cleans up expired cache entries.
*/
public async cleanUpExpiredEntries(): Promise<void> {
await this.db
Expand Down
24 changes: 17 additions & 7 deletions src/web5-connect/web5-connect-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,15 @@ export class Web5ConnectServer {
* Returns the Web5 Connect Request object. The request ID can only be used once.
*/
public async getWeb5ConnectRequest(requestId: string): Promise<Web5ConnectRequest | undefined> {
const request = this.cache.get(`request:${requestId}`);
const request = await this.cache.get(`request:${requestId}`);

// Delete the Request Object from the data store now that it has been retrieved.
this.cache.delete(`request:${requestId}`);
// Delete the Request Object from cache once it has been retrieved.
// IMPORTANT: only delete if the object exists, otherwise there could be a race condition
// where the object does not exist in this call but becomes available immediately after,
// we would end up deleting it before it is successfully retrieved.
if (request !== undefined) {
this.cache.delete(`request:${requestId}`);
}

return request;
}
Expand All @@ -102,10 +107,15 @@ export class Web5ConnectServer {
* Gets the Web5 Connect Response object. The `state` string can only be used once.
*/
public async getWeb5ConnectResponse(state: string): Promise<Web5ConnectResponse | undefined> {
const response = this. cache.get(`response:${state}`);

// Delete the Response object from the data store now that it has been retrieved.
this.cache.delete(`response:${state}`);
const response = await this.cache.get(`response:${state}`);

// Delete the Response object from the cache once it has been retrieved.
// IMPORTANT: only delete if the object exists, otherwise there could be a race condition
// where the object does not exist in this call but becomes available immediately after,
// we would end up deleting it before it is successfully retrieved.
if (response !== undefined) {
this.cache.delete(`response:${state}`);
}

return response;
}
Expand Down

0 comments on commit 704cbb4

Please sign in to comment.