494 lines
16 KiB
TypeScript
494 lines
16 KiB
TypeScript
|
|
/**
|
||
|
|
* S3-Compatible Storage Adapter
|
||
|
|
* Uses the AWS S3 client to interact with S3-compatible storage services
|
||
|
|
* including Amazon S3, Cloudflare R2, and Google Cloud Storage
|
||
|
|
*/
|
||
|
|
import { GraphVerb, HNSWNoun, HNSWVerb, StatisticsData } from '../../coreTypes.js';
|
||
|
|
import { BaseStorage } from '../baseStorage.js';
|
||
|
|
import { OperationConfig } from '../../utils/operationUtils.js';
|
||
|
|
type HNSWNode = HNSWNoun;
|
||
|
|
type Edge = HNSWVerb;
|
||
|
|
interface ChangeLogEntry {
|
||
|
|
timestamp: number;
|
||
|
|
operation: 'add' | 'update' | 'delete';
|
||
|
|
entityType: 'noun' | 'verb' | 'metadata';
|
||
|
|
entityId: string;
|
||
|
|
data?: any;
|
||
|
|
instanceId?: string;
|
||
|
|
}
|
||
|
|
export { S3CompatibleStorage as R2Storage };
|
||
|
|
/**
|
||
|
|
* S3-compatible storage adapter for server environments
|
||
|
|
* Uses the AWS S3 client to interact with S3-compatible storage services
|
||
|
|
* including Amazon S3, Cloudflare R2, and Google Cloud Storage
|
||
|
|
*
|
||
|
|
* To use this adapter with Amazon S3, you need to provide:
|
||
|
|
* - region: AWS region (e.g., 'us-east-1')
|
||
|
|
* - credentials: AWS credentials (accessKeyId and secretAccessKey)
|
||
|
|
* - bucketName: S3 bucket name
|
||
|
|
*
|
||
|
|
* To use this adapter with Cloudflare R2, you need to provide:
|
||
|
|
* - accountId: Cloudflare account ID
|
||
|
|
* - accessKeyId: R2 access key ID
|
||
|
|
* - secretAccessKey: R2 secret access key
|
||
|
|
* - bucketName: R2 bucket name
|
||
|
|
*
|
||
|
|
* To use this adapter with Google Cloud Storage, you need to provide:
|
||
|
|
* - region: GCS region (e.g., 'us-central1')
|
||
|
|
* - credentials: GCS credentials (accessKeyId and secretAccessKey)
|
||
|
|
* - endpoint: GCS endpoint (e.g., 'https://storage.googleapis.com')
|
||
|
|
* - bucketName: GCS bucket name
|
||
|
|
*/
|
||
|
|
export declare class S3CompatibleStorage extends BaseStorage {
|
||
|
|
private s3Client;
|
||
|
|
private bucketName;
|
||
|
|
private serviceType;
|
||
|
|
private region;
|
||
|
|
private endpoint?;
|
||
|
|
private accountId?;
|
||
|
|
private accessKeyId;
|
||
|
|
private secretAccessKey;
|
||
|
|
private sessionToken?;
|
||
|
|
private nounPrefix;
|
||
|
|
private verbPrefix;
|
||
|
|
private metadataPrefix;
|
||
|
|
private verbMetadataPrefix;
|
||
|
|
private indexPrefix;
|
||
|
|
private systemPrefix;
|
||
|
|
private useDualWrite;
|
||
|
|
protected statisticsCache: StatisticsData | null;
|
||
|
|
private lockPrefix;
|
||
|
|
private activeLocks;
|
||
|
|
private changeLogPrefix;
|
||
|
|
private pendingOperations;
|
||
|
|
private maxConcurrentOperations;
|
||
|
|
private baseBatchSize;
|
||
|
|
private currentBatchSize;
|
||
|
|
private lastMemoryCheck;
|
||
|
|
private memoryCheckInterval;
|
||
|
|
private consecutiveErrors;
|
||
|
|
private lastErrorReset;
|
||
|
|
private socketManager;
|
||
|
|
private backpressure;
|
||
|
|
private nounWriteBuffer;
|
||
|
|
private verbWriteBuffer;
|
||
|
|
private requestCoalescer;
|
||
|
|
private highVolumeMode;
|
||
|
|
private lastVolumeCheck;
|
||
|
|
private volumeCheckInterval;
|
||
|
|
private forceHighVolumeMode;
|
||
|
|
private operationExecutors;
|
||
|
|
private nounCacheManager;
|
||
|
|
private verbCacheManager;
|
||
|
|
private logger;
|
||
|
|
/**
|
||
|
|
* Initialize the storage adapter
|
||
|
|
* @param options Configuration options for the S3-compatible storage
|
||
|
|
*/
|
||
|
|
constructor(options: {
|
||
|
|
bucketName: string;
|
||
|
|
region?: string;
|
||
|
|
endpoint?: string;
|
||
|
|
accountId?: string;
|
||
|
|
accessKeyId: string;
|
||
|
|
secretAccessKey: string;
|
||
|
|
sessionToken?: string;
|
||
|
|
serviceType?: string;
|
||
|
|
operationConfig?: OperationConfig;
|
||
|
|
cacheConfig?: {
|
||
|
|
hotCacheMaxSize?: number;
|
||
|
|
hotCacheEvictionThreshold?: number;
|
||
|
|
warmCacheTTL?: number;
|
||
|
|
};
|
||
|
|
readOnly?: boolean;
|
||
|
|
});
|
||
|
|
/**
|
||
|
|
* Initialize the storage adapter
|
||
|
|
*/
|
||
|
|
init(): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Override base class method to detect S3-specific throttling errors
|
||
|
|
*/
|
||
|
|
protected isThrottlingError(error: any): boolean;
|
||
|
|
/**
|
||
|
|
* Override to add S3-specific logging
|
||
|
|
*/
|
||
|
|
handleThrottling(error: any, service?: string): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Smart delay based on current throttling status
|
||
|
|
*/
|
||
|
|
private smartDelay;
|
||
|
|
/**
|
||
|
|
* Auto-cleanup legacy /index folder during initialization
|
||
|
|
* This removes old index data that has been migrated to _system
|
||
|
|
*/
|
||
|
|
private cleanupLegacyIndexFolder;
|
||
|
|
/**
|
||
|
|
* Initialize write buffers for high-volume scenarios
|
||
|
|
*/
|
||
|
|
private initializeBuffers;
|
||
|
|
/**
|
||
|
|
* Initialize request coalescer
|
||
|
|
*/
|
||
|
|
private initializeCoalescer;
|
||
|
|
/**
|
||
|
|
* Check if we should enable high-volume mode
|
||
|
|
*/
|
||
|
|
private checkVolumeMode;
|
||
|
|
/**
|
||
|
|
* Bulk write nouns to S3
|
||
|
|
*/
|
||
|
|
private bulkWriteNouns;
|
||
|
|
/**
|
||
|
|
* Bulk write verbs to S3
|
||
|
|
*/
|
||
|
|
private bulkWriteVerbs;
|
||
|
|
/**
|
||
|
|
* Process coalesced batch of operations
|
||
|
|
*/
|
||
|
|
private processCoalescedBatch;
|
||
|
|
/**
|
||
|
|
* Process bulk deletes
|
||
|
|
*/
|
||
|
|
private processBulkDeletes;
|
||
|
|
/**
|
||
|
|
* Process bulk writes
|
||
|
|
*/
|
||
|
|
private processBulkWrites;
|
||
|
|
/**
|
||
|
|
* Process bulk reads
|
||
|
|
*/
|
||
|
|
private processBulkReads;
|
||
|
|
/**
|
||
|
|
* Dynamically adjust batch size based on memory pressure and error rates
|
||
|
|
*/
|
||
|
|
private adjustBatchSize;
|
||
|
|
/**
|
||
|
|
* Apply backpressure when system is under load
|
||
|
|
*/
|
||
|
|
private applyBackpressure;
|
||
|
|
/**
|
||
|
|
* Release backpressure after operation completes
|
||
|
|
*/
|
||
|
|
private releaseBackpressure;
|
||
|
|
/**
|
||
|
|
* Get current batch size for operations
|
||
|
|
*/
|
||
|
|
private getBatchSize;
|
||
|
|
/**
|
||
|
|
* Save a noun to storage (internal implementation)
|
||
|
|
*/
|
||
|
|
protected saveNoun_internal(noun: HNSWNoun): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Save a node to storage
|
||
|
|
*/
|
||
|
|
protected saveNode(node: HNSWNode): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Get a noun from storage (internal implementation)
|
||
|
|
*/
|
||
|
|
protected getNoun_internal(id: string): Promise<HNSWNoun | null>;
|
||
|
|
/**
|
||
|
|
* Get a node from storage
|
||
|
|
*/
|
||
|
|
protected getNode(id: string): Promise<HNSWNode | null>;
|
||
|
|
private nodeCache;
|
||
|
|
/**
|
||
|
|
* Get all nodes from storage
|
||
|
|
* @deprecated This method is deprecated and will be removed in a future version.
|
||
|
|
* It can cause memory issues with large datasets. Use getNodesWithPagination() instead.
|
||
|
|
*/
|
||
|
|
protected getAllNodes(): Promise<HNSWNode[]>;
|
||
|
|
/**
|
||
|
|
* Get nodes with pagination
|
||
|
|
* @param options Pagination options
|
||
|
|
* @returns Promise that resolves to a paginated result of nodes
|
||
|
|
*/
|
||
|
|
protected getNodesWithPagination(options?: {
|
||
|
|
limit?: number;
|
||
|
|
cursor?: string;
|
||
|
|
useCache?: boolean;
|
||
|
|
}): Promise<{
|
||
|
|
nodes: HNSWNode[];
|
||
|
|
hasMore: boolean;
|
||
|
|
nextCursor?: string;
|
||
|
|
}>;
|
||
|
|
/**
|
||
|
|
* Get nouns by noun type (internal implementation)
|
||
|
|
* @param nounType The noun type to filter by
|
||
|
|
* @returns Promise that resolves to an array of nouns of the specified noun type
|
||
|
|
*/
|
||
|
|
protected getNounsByNounType_internal(nounType: string): Promise<HNSWNoun[]>;
|
||
|
|
/**
|
||
|
|
* Get nodes by noun type
|
||
|
|
* @param nounType The noun type to filter by
|
||
|
|
* @returns Promise that resolves to an array of nodes of the specified noun type
|
||
|
|
*/
|
||
|
|
protected getNodesByNounType(nounType: string): Promise<HNSWNode[]>;
|
||
|
|
/**
|
||
|
|
* Delete a noun from storage (internal implementation)
|
||
|
|
*/
|
||
|
|
protected deleteNoun_internal(id: string): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Delete a node from storage
|
||
|
|
*/
|
||
|
|
protected deleteNode(id: string): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Save a verb to storage (internal implementation)
|
||
|
|
*/
|
||
|
|
protected saveVerb_internal(verb: HNSWVerb): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Save an edge to storage
|
||
|
|
*/
|
||
|
|
protected saveEdge(edge: Edge): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Get a verb from storage (internal implementation)
|
||
|
|
*/
|
||
|
|
protected getVerb_internal(id: string): Promise<HNSWVerb | null>;
|
||
|
|
/**
|
||
|
|
* Get an edge from storage
|
||
|
|
*/
|
||
|
|
protected getEdge(id: string): Promise<Edge | null>;
|
||
|
|
/**
|
||
|
|
* Get all edges from storage
|
||
|
|
* @deprecated This method is deprecated and will be removed in a future version.
|
||
|
|
* It can cause memory issues with large datasets. Use getEdgesWithPagination() instead.
|
||
|
|
*/
|
||
|
|
protected getAllEdges(): Promise<Edge[]>;
|
||
|
|
/**
|
||
|
|
* Get edges with pagination
|
||
|
|
* @param options Pagination options
|
||
|
|
* @returns Promise that resolves to a paginated result of edges
|
||
|
|
*/
|
||
|
|
protected getEdgesWithPagination(options?: {
|
||
|
|
limit?: number;
|
||
|
|
cursor?: string;
|
||
|
|
useCache?: boolean;
|
||
|
|
filter?: {
|
||
|
|
sourceId?: string;
|
||
|
|
targetId?: string;
|
||
|
|
type?: string;
|
||
|
|
};
|
||
|
|
}): Promise<{
|
||
|
|
edges: Edge[];
|
||
|
|
hasMore: boolean;
|
||
|
|
nextCursor?: string;
|
||
|
|
}>;
|
||
|
|
/**
|
||
|
|
* Filter an edge based on filter criteria
|
||
|
|
* @param edge The edge to filter
|
||
|
|
* @param filter The filter criteria
|
||
|
|
* @returns True if the edge matches the filter, false otherwise
|
||
|
|
*/
|
||
|
|
private filterEdge;
|
||
|
|
/**
|
||
|
|
* Get verbs with pagination
|
||
|
|
* @param options Pagination options
|
||
|
|
* @returns Promise that resolves to a paginated result of verbs
|
||
|
|
*/
|
||
|
|
getVerbsWithPagination(options?: {
|
||
|
|
limit?: number;
|
||
|
|
cursor?: string;
|
||
|
|
filter?: {
|
||
|
|
verbType?: string | string[];
|
||
|
|
sourceId?: string | string[];
|
||
|
|
targetId?: string | string[];
|
||
|
|
service?: string | string[];
|
||
|
|
metadata?: Record<string, any>;
|
||
|
|
};
|
||
|
|
}): Promise<{
|
||
|
|
items: GraphVerb[];
|
||
|
|
totalCount?: number;
|
||
|
|
hasMore: boolean;
|
||
|
|
nextCursor?: string;
|
||
|
|
}>;
|
||
|
|
/**
|
||
|
|
* Get verbs by source (internal implementation)
|
||
|
|
*/
|
||
|
|
protected getVerbsBySource_internal(sourceId: string): Promise<GraphVerb[]>;
|
||
|
|
/**
|
||
|
|
* Get verbs by target (internal implementation)
|
||
|
|
*/
|
||
|
|
protected getVerbsByTarget_internal(targetId: string): Promise<GraphVerb[]>;
|
||
|
|
/**
|
||
|
|
* Get verbs by type (internal implementation)
|
||
|
|
*/
|
||
|
|
protected getVerbsByType_internal(type: string): Promise<GraphVerb[]>;
|
||
|
|
/**
|
||
|
|
* Delete a verb from storage (internal implementation)
|
||
|
|
*/
|
||
|
|
protected deleteVerb_internal(id: string): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Delete an edge from storage
|
||
|
|
*/
|
||
|
|
protected deleteEdge(id: string): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Save metadata to storage
|
||
|
|
*/
|
||
|
|
saveMetadata(id: string, metadata: any): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Save verb metadata to storage
|
||
|
|
*/
|
||
|
|
saveVerbMetadata(id: string, metadata: any): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Get verb metadata from storage
|
||
|
|
*/
|
||
|
|
getVerbMetadata(id: string): Promise<any | null>;
|
||
|
|
/**
|
||
|
|
* Save noun metadata to storage
|
||
|
|
*/
|
||
|
|
saveNounMetadata(id: string, metadata: any): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Get multiple metadata objects in batches (CRITICAL: Prevents socket exhaustion)
|
||
|
|
* This is the solution to the metadata reading socket exhaustion during initialization
|
||
|
|
*/
|
||
|
|
getMetadataBatch(ids: string[]): Promise<Map<string, any>>;
|
||
|
|
/**
|
||
|
|
* Get multiple verb metadata objects in batches (prevents socket exhaustion)
|
||
|
|
*/
|
||
|
|
getVerbMetadataBatch(ids: string[]): Promise<Map<string, any>>;
|
||
|
|
/**
|
||
|
|
* Get noun metadata from storage
|
||
|
|
*/
|
||
|
|
getNounMetadata(id: string): Promise<any | null>;
|
||
|
|
/**
|
||
|
|
* Get metadata from storage
|
||
|
|
*/
|
||
|
|
getMetadata(id: string): Promise<any | null>;
|
||
|
|
/**
|
||
|
|
* Clear all data from storage
|
||
|
|
*/
|
||
|
|
clear(): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Get information about storage usage and capacity
|
||
|
|
* Optimized version that uses cached statistics instead of expensive full scans
|
||
|
|
*/
|
||
|
|
getStorageStatus(): Promise<{
|
||
|
|
type: string;
|
||
|
|
used: number;
|
||
|
|
quota: number | null;
|
||
|
|
details?: Record<string, any>;
|
||
|
|
}>;
|
||
|
|
protected statisticsBatchUpdateTimerId: NodeJS.Timeout | null;
|
||
|
|
protected statisticsModified: boolean;
|
||
|
|
protected lastStatisticsFlushTime: number;
|
||
|
|
protected readonly MIN_FLUSH_INTERVAL_MS = 5000;
|
||
|
|
protected readonly MAX_FLUSH_DELAY_MS = 30000;
|
||
|
|
/**
|
||
|
|
* Get the statistics key for a specific date
|
||
|
|
* @param date The date to get the key for
|
||
|
|
* @returns The statistics key for the specified date
|
||
|
|
*/
|
||
|
|
private getStatisticsKeyForDate;
|
||
|
|
/**
|
||
|
|
* Get the current statistics key
|
||
|
|
* @returns The current statistics key
|
||
|
|
*/
|
||
|
|
private getCurrentStatisticsKey;
|
||
|
|
/**
|
||
|
|
* Get the legacy statistics key (DEPRECATED - /index folder is auto-cleaned)
|
||
|
|
* @returns The legacy statistics key
|
||
|
|
* @deprecated Legacy /index folder is automatically cleaned on initialization
|
||
|
|
*/
|
||
|
|
private getLegacyStatisticsKey;
|
||
|
|
/**
|
||
|
|
* Schedule a batch update of statistics
|
||
|
|
*/
|
||
|
|
protected scheduleBatchUpdate(): void;
|
||
|
|
/**
|
||
|
|
* Flush statistics to storage with distributed locking
|
||
|
|
*/
|
||
|
|
protected flushStatistics(): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Merge statistics from storage with local statistics
|
||
|
|
* @param storageStats Statistics from storage
|
||
|
|
* @param localStats Local statistics to merge
|
||
|
|
* @returns Merged statistics data
|
||
|
|
*/
|
||
|
|
private mergeStatistics;
|
||
|
|
/**
|
||
|
|
* Save statistics data to storage
|
||
|
|
* @param statistics The statistics data to save
|
||
|
|
*/
|
||
|
|
protected saveStatisticsData(statistics: StatisticsData): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Get statistics data from storage
|
||
|
|
* @returns Promise that resolves to the statistics data or null if not found
|
||
|
|
*/
|
||
|
|
protected getStatisticsData(): Promise<StatisticsData | null>;
|
||
|
|
/**
|
||
|
|
* Check if we should try yesterday's statistics file
|
||
|
|
* Only try within 2 hours of midnight to avoid unnecessary calls
|
||
|
|
*/
|
||
|
|
private shouldTryYesterday;
|
||
|
|
/**
|
||
|
|
* Get yesterday's date
|
||
|
|
*/
|
||
|
|
private getYesterday;
|
||
|
|
/**
|
||
|
|
* Try to get statistics from a specific key
|
||
|
|
* @param key The key to try to get statistics from
|
||
|
|
* @returns The statistics data or null if not found
|
||
|
|
*/
|
||
|
|
private tryGetStatisticsFromKey;
|
||
|
|
/**
|
||
|
|
* Append an entry to the change log for efficient synchronization
|
||
|
|
* @param entry The change log entry to append
|
||
|
|
*/
|
||
|
|
private appendToChangeLog;
|
||
|
|
/**
|
||
|
|
* Get changes from the change log since a specific timestamp
|
||
|
|
* @param sinceTimestamp Timestamp to get changes since
|
||
|
|
* @param maxEntries Maximum number of entries to return (default: 1000)
|
||
|
|
* @returns Array of change log entries
|
||
|
|
*/
|
||
|
|
getChangesSince(sinceTimestamp: number, maxEntries?: number): Promise<ChangeLogEntry[]>;
|
||
|
|
/**
|
||
|
|
* Clean up old change log entries to prevent unlimited growth
|
||
|
|
* @param olderThanTimestamp Remove entries older than this timestamp
|
||
|
|
*/
|
||
|
|
cleanupOldChangeLogs(olderThanTimestamp: number): Promise<void>;
|
||
|
|
/**
|
||
|
|
* Sample-based storage estimation as fallback when statistics unavailable
|
||
|
|
* Much faster than full scans - samples first 50 objects per prefix
|
||
|
|
*/
|
||
|
|
private getSampleBasedStorageEstimate;
|
||
|
|
/**
|
||
|
|
* Acquire a distributed lock for coordinating operations across multiple instances
|
||
|
|
* @param lockKey The key to lock on
|
||
|
|
* @param ttl Time to live for the lock in milliseconds (default: 30 seconds)
|
||
|
|
* @returns Promise that resolves to true if lock was acquired, false otherwise
|
||
|
|
*/
|
||
|
|
private acquireLock;
|
||
|
|
/**
|
||
|
|
* Release a distributed lock
|
||
|
|
* @param lockKey The key to unlock
|
||
|
|
* @param lockValue The value used when acquiring the lock (for verification)
|
||
|
|
* @returns Promise that resolves when lock is released
|
||
|
|
*/
|
||
|
|
private releaseLock;
|
||
|
|
/**
|
||
|
|
* Clean up expired locks to prevent lock leakage
|
||
|
|
* This method should be called periodically
|
||
|
|
*/
|
||
|
|
private cleanupExpiredLocks;
|
||
|
|
/**
|
||
|
|
* Get nouns with pagination support
|
||
|
|
* @param options Pagination options
|
||
|
|
* @returns Promise that resolves to a paginated result of nouns
|
||
|
|
*/
|
||
|
|
getNounsWithPagination(options?: {
|
||
|
|
limit?: number;
|
||
|
|
cursor?: string;
|
||
|
|
filter?: {
|
||
|
|
nounType?: string | string[];
|
||
|
|
service?: string | string[];
|
||
|
|
metadata?: Record<string, any>;
|
||
|
|
};
|
||
|
|
}): Promise<{
|
||
|
|
items: HNSWNoun[];
|
||
|
|
totalCount?: number;
|
||
|
|
hasMore: boolean;
|
||
|
|
nextCursor?: string;
|
||
|
|
}>;
|
||
|
|
}
|