Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

Commit

Permalink
feat(redis): implement redis cache for arns name resolutions
Browse files Browse the repository at this point in the history
  • Loading branch information
dtfiedler committed Sep 4, 2024
1 parent 7fe02ec commit 28e2798
Show file tree
Hide file tree
Showing 12 changed files with 450 additions and 261 deletions.
35 changes: 35 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
services:
resolver:
image: ghcr.io/ar-io/arns-resolver:${RESOLVER_IMAGE_TAG:-7fe02ecda2027e504248d3f3716579f60b561de5}
build:
context: .
restart: on-failure
ports:
- ${HOST_PORT:-6000}:${CONTAINER_PORT:-6000}
environment:
- PORT=${CONTAINER_PORT:-6000}
- LOG_LEVEL=${LOG_LEVEL:-info}
- IO_PROCESS_ID=${IO_PROCESS_ID:-}
- RUN_RESOLVER=${RUN_RESOLVER:-true}
- EVALUATION_INTERVAL_MS=${EVALUATION_INTERVAL_MS:-}
- ARNS_CACHE_TTL_MS=${RESOLVER_CACHE_TTL_MS:-}
- ARNS_CACHE_PATH=${ARNS_CACHE_PATH:-./data/arns}
- ARNS_CACHE_TYPE=${ARNS_CACHE_TYPE:-redis}
- REDIS_CACHE_URL=${REDIS_CACHE_URL:-redis://redis:6379}
- AO_CU_URL=${AO_CU_URL:-}
- AO_MU_URL=${AO_MU_URL:-}
- AO_GATEWAY_URL=${AO_GATEWAY_URL:-}
- AO_GRAPHQL_URL=${AO_GRAPHQL_URL:-}
volumes:
- ${ARNS_CACHE_PATH:-./data/arns}:/app/data/arns
depends_on:
- redis

redis:
image: redis:${REDIS_IMAGE_TAG:-7}
restart: on-failure
ports:
- 6379:6379
volumes:
- ${REDIS_DATA_PATH:-./data/redis}:/data

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"url": "https://github.com/ar-io/arns-resolver"
},
"dependencies": {
"@ar.io/sdk": "^2.0.2",
"@ar.io/sdk": "^2.1.0",
"@permaweb/aoconnect": "^0.0.56",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
Expand All @@ -21,6 +21,7 @@
"middleware-async": "^1.3.5",
"p-limit": "^4.0.0",
"prom-client": "^14.0.1",
"redis": "^4.7.0",
"swagger-ui-express": "^5.0.0",
"winston": "^3.7.2",
"yaml": "^2.3.1"
Expand All @@ -33,6 +34,7 @@
"@types/express-prometheus-middleware": "^1.2.1",
"@types/jest": "^29.5.12",
"@types/node": "^16.11.7",
"@types/redis": "^4.0.11",
"@types/swagger-ui-express": "^4.1.3",
"@typescript-eslint/eslint-plugin": "^5.26.0",
"@typescript-eslint/parser": "^5.26.0",
Expand Down
69 changes: 69 additions & 0 deletions src/cache/arns-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* AR.IO ArNS Resolver
* Copyright (C) 2023 Permanent Data Solutions, Inc. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import winston from 'winston';

import { KVBufferStore } from '../types.js';

export class ArNSStore implements KVBufferStore {
private log: winston.Logger;
private prefix: string;
private kvStore: KVBufferStore;

constructor({
log,
kvStore,
prefix = 'ArNS',
}: {
log: winston.Logger;
kvStore: KVBufferStore;
prefix?: string;
}) {
this.log = log.child({ class: this.constructor.name });
this.kvStore = kvStore;
this.prefix = prefix;
this.log.info('ArNSStore initialized', {
prefix,
kvStore: kvStore.constructor.name,
});
}

// avoid collisions with other redis keys
private hashKey(key: string): string {
return `${this.prefix}|${key}`;
}

async get(key: string): Promise<Buffer | undefined> {
return this.kvStore.get(this.hashKey(key));
}

async set(key: string, value: Buffer, ttlSeconds?: number): Promise<void> {
return this.kvStore.set(this.hashKey(key), value, ttlSeconds);
}

async del(key: string): Promise<void> {
return this.kvStore.del(this.hashKey(key));
}

async has(key: string): Promise<boolean> {
return this.kvStore.has(this.hashKey(key));
}

async close(): Promise<void> {
return this.kvStore.close();
}
}
4 changes: 4 additions & 0 deletions src/cache/lmdb-kv-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,8 @@ export class LmdbKVStore implements KVBufferStore {
async set(key: string, buffer: Buffer): Promise<void> {
await this.db.put(key, this.serialize(buffer));
}

async close(): Promise<void> {
await this.db.close();
}
}
80 changes: 80 additions & 0 deletions src/cache/redis-kv-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* AR.IO ArNS Resolver
* Copyright (C) 2023 Permanent Data Solutions, Inc. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { RedisClientType, commandOptions, createClient } from 'redis';
import winston from 'winston';

import { KVBufferStore } from '../types.js';

export class RedisKvStore implements KVBufferStore {
private client: RedisClientType;
private log: winston.Logger;
private defaultTtlSeconds?: number;

constructor({ log, redisUrl }: { log: winston.Logger; redisUrl: string }) {
this.log = log.child({ class: this.constructor.name });
this.client = createClient({
url: redisUrl,
});
this.client.on('error', (error: any) => {
this.log.error(`Redis error`, {
message: error.message,
stack: error.stack,
});
// TODO: add prometheus metric for redis error
});
this.client.connect().catch((error: any) => {
this.log.error(`Redis connection error`, {
message: error.message,
stack: error.stack,
});
// TODO: add prometheus metric for redis connection error
});
}

async close() {
await this.client.quit();
}

async get(key: string): Promise<Buffer | undefined> {
const value = await this.client.get(
commandOptions({ returnBuffers: true }),
key,
);
return value ?? undefined;
}

async has(key: string): Promise<boolean> {
return (await this.client.exists(key)) === 1;
}

async del(key: string): Promise<void> {
if (await this.has(key)) {
await this.client.del(key);
}
}

async set(key: string, buffer: Buffer, ttlSeconds?: number): Promise<void> {
if (ttlSeconds !== undefined) {
await this.client.set(key, buffer, {
EX: ttlSeconds ?? this.defaultTtlSeconds,
});
} else {
await this.client.set(key, buffer);
}
}
}
5 changes: 5 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export const ARNS_CACHE_TTL_MS = +env.varOrDefault(
'ARNS_CACHE_TTL_MS',
`${1000 * 60 * 60}`, // 1 hour by default
);
export const ARNS_CACHE_TYPE = env.varOrDefault('ARNS_CACHE_TYPE', 'lmdb');
export const REDIS_CACHE_URL = env.varOrDefault(
'REDIS_CACHE_URL',
'redis://localhost:6379',
);
export const RUN_RESOLVER = env.varOrDefault('RUN_RESOLVER', 'true') === 'true';
export const ENABLE_OPENAPI_VALIDATION =
env.varOrDefault('ENABLE_OPENAPI_VALIDATION', 'true') === 'true';
Expand Down
51 changes: 51 additions & 0 deletions src/lib/kv-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* AR.IO ArNS Resolver
* Copyright (C) 2023 Permanent Data Solutions, Inc. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import winston from 'winston';

import { LmdbKVStore } from '../cache/lmdb-kv-store.js';
import { RedisKvStore } from '../cache/redis-kv-store.js';

function isSupportedKvStoreType(type: string): type is 'lmdb' | 'redis' {
return type === 'lmdb' || type === 'redis';
}

export const createKvStore = ({
log,
type,
path,
redisUrl,
ttlSeconds,
}: {
log: winston.Logger;
type: 'lmdb' | 'redis' | string;
path: string;
redisUrl: string;
ttlSeconds?: number;
}) => {
if (!isSupportedKvStoreType(type)) {
throw new Error(`Unknown kv store type: ${type}`);
}
switch (type) {
case 'lmdb':
return new LmdbKVStore({ dbPath: path, ttlSeconds });
case 'redis':
return new RedisKvStore({ redisUrl, log });
default:
throw new Error(`Unknown kv store type: ${type}`);
}
};
Loading

0 comments on commit 28e2798

Please sign in to comment.