Skip to content

Commit

Permalink
feat: improve region cache
Browse files Browse the repository at this point in the history
  • Loading branch information
lihsai0 committed Nov 1, 2024
1 parent 09fd2df commit 5767cc4
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 47 deletions.
3 changes: 3 additions & 0 deletions adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { URL } from 'url';
import { Readable } from 'stream';
import { OutgoingHttpHeaders } from 'http';
import { NatureLanguage } from './uplog';
import { KodoHttpClient } from './kodo-http-client';

export abstract class Adapter {
abstract readonly client: KodoHttpClient;

abstract storageClasses: StorageClass[];

abstract enter<T>(sdkApiName: string, f: (scope: Adapter) => Promise<T>, sdkUplogOption?: EnterUplogOption): Promise<T>;
Expand Down
137 changes: 95 additions & 42 deletions kodo-http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,15 @@ export interface SharedRequestOptions extends AdapterOption {
apiType: 'kodo' | 's3',
}

const regionsCache: Map<string, Region> = new Map<string, Region>();
const regionsCacheLock = new AsyncLock();

export class KodoHttpClient {
private readonly regionsCache: { [key: string]: Region; } = {};
private readonly regionsCacheLock = new AsyncLock();
static clearCache(): void {
regionsCache.clear();
RegionService.clearCache();
}

private readonly regionService: RegionService;
private static logClientId: string | undefined = undefined;
private readonly uplogBuffer: UplogBuffer;
Expand All @@ -69,7 +75,12 @@ export class KodoHttpClient {
}

async call<T = any>(options: RequestOptions): Promise<HttpClientResponse<T>> {
const urls = await this.getServiceUrls(options.serviceName, options.bucketName, options.s3RegionId, options.stats);
const urls = await this.getServiceUrls({
serviceName: options.serviceName,
bucketName: options.bucketName,
s3RegionId: options.s3RegionId,
stats: options.stats
});
return await this.callUrls(urls, {
method: options.method,
path: options.path,
Expand All @@ -95,50 +106,82 @@ export class KodoHttpClient {
}

clearCache() {
Object.keys(this.regionsCache).forEach((key) => { delete this.regionsCache[key]; });
this.regionService.clearCache();
KodoHttpClient.clearCache();
}

private async getServiceUrls(
private async getRegion(
serviceName: ServiceName,
bucketName?: string,
s3RegionId?: string,
stats?: RequestStats,
): Promise<Region> {
let key: string;
if (s3RegionId) {
key = `${this.sharedOptions.ucUrl}/${s3RegionId}`;
} else {
key = `${this.sharedOptions.ucUrl}/${this.sharedOptions.accessKey}/${bucketName}`;
}
if (serviceName === ServiceName.UpAcc) {
key += `/${ServiceName.UpAcc}`;
}
const cachedRegion = regionsCache.get(key);
if (cachedRegion?.validated) {
return cachedRegion;
}
return await regionsCacheLock.acquire(key, async (): Promise<Region> => {
// re-check cache by others may fetch it
const cachedRegion = regionsCache.get(key);
if (cachedRegion?.validated) {
return cachedRegion;
}

try {
const fetchedRegion = await this.fetchRegion(
bucketName,
s3RegionId,
stats,
);
if (
bucketName &&
serviceName === ServiceName.UpAcc &&
!fetchedRegion.upAccUrls?.length
) {
// no upAccUrls, do not cache
return fetchedRegion;
}
regionsCache.set(key, fetchedRegion);
return fetchedRegion;
} catch (err) {
// when err, still return expired cached region
if (cachedRegion && !cachedRegion.validated) {
return cachedRegion;
}
throw err;
}
});
}

async getServiceUrls({
serviceName,
bucketName,
s3RegionId,
stats,
withFallback = true,
}:{
serviceName: ServiceName,
bucketName?: string,
s3RegionId?: string,
stats?: RequestStats,
): Promise<string[]> {
let key: string;
if (s3RegionId) {
key = `${this.sharedOptions.ucUrl}/${s3RegionId}`;
} else {
key = `${this.sharedOptions.ucUrl}/${this.sharedOptions.accessKey}/${bucketName}`;
}
if (this.regionsCache[key]?.validated) {
return this.getUrlsFromRegion(serviceName, this.regionsCache[key]);
}
const region: Region = await this.regionsCacheLock.acquire(key, async (): Promise<Region> => {
// re-check cache by others may fetch it
const cachedRegion = this.regionsCache[key];
if (cachedRegion?.validated) {
return cachedRegion;
}

try {
const fetchedRegion = await this.fetchRegion(
bucketName,
s3RegionId,
stats,
);
this.regionsCache[key] = fetchedRegion;
return fetchedRegion;
} catch (err) {
// when err, still return expired cached region
if (cachedRegion && !cachedRegion.validated) {
return cachedRegion;
}
throw err;
}
});
withFallback?: boolean,
}): Promise<string[]> {
const region = await this.getRegion(
serviceName,
bucketName,
s3RegionId,
stats,
);

return this.getUrlsFromRegion(serviceName, region);
return this.getUrlsFromRegion(serviceName, region, withFallback);
}

private async fetchRegion(
Expand Down Expand Up @@ -231,15 +274,25 @@ export class KodoHttpClient {
}
}

private getUrlsFromRegion(serviceName: ServiceName, region: Region): string[] {
private getUrlsFromRegion(
serviceName: ServiceName,
region: Region,
withFallback = true,
): string[] {
switch (serviceName) {
case ServiceName.Up:
return [...region.upUrls];
case ServiceName.UpAcc:
if (!withFallback) {
return [...region.upAccUrls];
}
if (!region.upAccUrls.length) {
return [...region.upUrls];
}
return [...region.upAccUrls];
return [
...region.upAccUrls,
...region.upUrls,
];
case ServiceName.Uc:
return [...region.ucUrls];
case ServiceName.Rs:
Expand Down
2 changes: 1 addition & 1 deletion kodo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ interface KodoAdapterOption {

export class Kodo implements Adapter {
storageClasses: StorageClass[] = [];
protected readonly client: KodoHttpClient;
readonly client: KodoHttpClient;
protected readonly regionService: RegionService;
protected bucketDomainsCache: Record<string, Domain[]> = {};
protected bucketDomainsCacheLock = new AsyncLock();
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kodo-s3-adapter-sdk",
"version": "0.6.1",
"version": "0.7.0",
"description": "Adapter for Kodo & S3 API",
"main": "dist/index.js",
"repository": "github.com/bachue/kodo-s3-adapter-sdk",
Expand Down
8 changes: 6 additions & 2 deletions region.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,16 @@ export class Region {
readonly label?: string,
readonly translatedLabels: { [lang: string]: string; } = {},
readonly storageClasses: RegionStorageClass[] = [],
readonly ttl: number = 0,
public ttl: number = 0,
readonly createTime: number = Date.now(),
) {}

get validated(): boolean {
return Date.now() < (this.createTime + this.ttl * 1000);
if (this.ttl < 0) {
return true;
}
const liveTime = Math.round((Date.now() - this.createTime) / 1000);
return liveTime < this.ttl;
}

private static requestAll(options: GetAllOptions): Promise<HttpClientResponse<any>> {
Expand Down
5 changes: 4 additions & 1 deletion region_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const regionCache: Map<string, Region[]> = new Map<string, Region[]>();
const queryRegionLock = new AsyncLock();

export class RegionService {
static clearCache() {
regionCache.clear();
}

constructor(private readonly adapterOption: AdapterOption) {
}
Expand Down Expand Up @@ -59,7 +62,7 @@ export class RegionService {
}

clearCache() {
regionCache.clear();
RegionService.clearCache();
}

async getS3Endpoint(s3RegionId?: string, options?: GetAllRegionsOptions): Promise<S3IdEndpoint> {
Expand Down

0 comments on commit 5767cc4

Please sign in to comment.