**feat: implement robust error-handling and operation utilities for storage adapters**

- Added `BrainyError` class to classify and handle errors with types like `TIMEOUT`, `NETWORK`, `STORAGE`, `NOT_FOUND`, and `RETRY_EXHAUSTED`. Includes static helper methods for error creation and retry determination.
- Introduced `operationUtils` with utility functions for timeout, retry logic, and exponential backoff. Implements features like `withTimeout`, `withRetry`, and a combined `withTimeoutAndRetry`.
- Updated `S3CompatibleStorage` to leverage new operation utilities for timeout and retry handling, including `StorageOperationExecutors` for clean operation execution.
- Enhanced `storageFactory` to pass `OperationConfig` for configurable timeout and retry behavior.
- Extended `BrainyData` to include timeout and retry policy configuration at initialization.

**Purpose**: Improve storage reliability by introducing configurable and reusable error-handling and operation utilities, reducing code duplication and enhancing maintainability.
This commit is contained in:
David Snelling 2025-07-30 11:35:09 -07:00
parent db67ccd34f
commit e8127c54a5
5 changed files with 516 additions and 54 deletions

View file

@ -6,6 +6,8 @@
import {GraphVerb, HNSWNoun, StatisticsData} from '../../coreTypes.js'
import {BaseStorage, NOUNS_DIR, VERBS_DIR, METADATA_DIR, INDEX_DIR, STATISTICS_KEY} from '../baseStorage.js'
import {StorageOperationExecutors, OperationConfig} from '../../utils/operationUtils.js'
import {BrainyError} from '../../errors/brainyError.js'
// Type aliases for better readability
type HNSWNode = HNSWNoun
@ -76,6 +78,9 @@ export class S3CompatibleStorage extends BaseStorage {
// Change log for efficient synchronization
private changeLogPrefix: string = 'change-log/'
// Operation executors for timeout and retry handling
private operationExecutors: StorageOperationExecutors
/**
* Initialize the storage adapter
@ -90,6 +95,7 @@ export class S3CompatibleStorage extends BaseStorage {
secretAccessKey: string
sessionToken?: string
serviceType?: string
operationConfig?: OperationConfig
}) {
super()
this.bucketName = options.bucketName
@ -101,6 +107,9 @@ export class S3CompatibleStorage extends BaseStorage {
this.sessionToken = options.sessionToken
this.serviceType = options.serviceType || 's3'
// Initialize operation executors with timeout and retry configuration
this.operationExecutors = new StorageOperationExecutors(options.operationConfig)
// Set up prefixes for different types of data
this.nounPrefix = `${NOUNS_DIR}/`
this.verbPrefix = `${VERBS_DIR}/`
@ -938,61 +947,62 @@ export class S3CompatibleStorage extends BaseStorage {
public async getMetadata(id: string): Promise<any | null> {
await this.ensureInitialized()
try {
// Import the GetObjectCommand only when needed
const {GetObjectCommand} = await import('@aws-sdk/client-s3')
console.log(`Getting metadata for ${id} from bucket ${this.bucketName}`)
const key = `${this.metadataPrefix}${id}.json`
console.log(`Looking for metadata at key: ${key}`)
// Try to get the metadata from the metadata directory
const response = await this.s3Client!.send(
new GetObjectCommand({
Bucket: this.bucketName,
Key: key
})
)
// Check if response is null or undefined (can happen in mock implementations)
if (!response || !response.Body) {
console.log(`No metadata found for ${id}`)
return null
}
// Convert the response body to a string
const bodyContents = await response.Body.transformToString()
console.log(`Retrieved metadata body: ${bodyContents}`)
// Parse the JSON string
return this.operationExecutors.executeGet(async () => {
try {
const parsedMetadata = JSON.parse(bodyContents)
console.log(`Successfully retrieved metadata for ${id}:`, parsedMetadata)
return parsedMetadata
} catch (parseError) {
console.error(`Failed to parse metadata for ${id}:`, parseError)
return null
}
} catch (error: any) {
// Check if this is a "NoSuchKey" error (object doesn't exist)
// In AWS SDK, this would be error.name === 'NoSuchKey'
// In our mock, we might get different error types
if (
error.name === 'NoSuchKey' ||
(error.message && (
error.message.includes('NoSuchKey') ||
error.message.includes('not found') ||
error.message.includes('does not exist')
))
) {
console.log(`Metadata not found for ${id}`)
return null
}
// Import the GetObjectCommand only when needed
const {GetObjectCommand} = await import('@aws-sdk/client-s3')
// For other types of errors, log and re-throw
console.error(`Error getting metadata for ${id}:`, error)
throw error
}
console.log(`Getting metadata for ${id} from bucket ${this.bucketName}`)
const key = `${this.metadataPrefix}${id}.json`
console.log(`Looking for metadata at key: ${key}`)
// Try to get the metadata from the metadata directory
const response = await this.s3Client!.send(
new GetObjectCommand({
Bucket: this.bucketName,
Key: key
})
)
// Check if response is null or undefined (can happen in mock implementations)
if (!response || !response.Body) {
console.log(`No metadata found for ${id}`)
return null
}
// Convert the response body to a string
const bodyContents = await response.Body.transformToString()
console.log(`Retrieved metadata body: ${bodyContents}`)
// Parse the JSON string
try {
const parsedMetadata = JSON.parse(bodyContents)
console.log(`Successfully retrieved metadata for ${id}:`, parsedMetadata)
return parsedMetadata
} catch (parseError) {
console.error(`Failed to parse metadata for ${id}:`, parseError)
return null
}
} catch (error: any) {
// Check if this is a "NoSuchKey" error (object doesn't exist)
// In AWS SDK, this would be error.name === 'NoSuchKey'
// In our mock, we might get different error types
if (
error.name === 'NoSuchKey' ||
(error.message && (
error.message.includes('NoSuchKey') ||
error.message.includes('not found') ||
error.message.includes('does not exist')
))
) {
console.log(`Metadata not found for ${id}`)
return null
}
// For other types of errors, convert to BrainyError for better classification
throw BrainyError.fromError(error, `getMetadata(${id})`)
}
}, `getMetadata(${id})`)
}
/**

View file

@ -9,6 +9,7 @@ import {OPFSStorage} from './adapters/opfsStorage.js'
import {S3CompatibleStorage, R2Storage} from './adapters/s3CompatibleStorage.js'
import {FileSystemStorage} from './adapters/fileSystemStorage.js'
import {isBrowser} from '../utils/environment.js'
import {OperationConfig} from '../utils/operationUtils.js'
/**
* Options for creating a storage adapter
@ -165,6 +166,11 @@ export interface StorageOptions {
*/
serviceType?: string
}
/**
* Operation configuration for timeout and retry behavior
*/
operationConfig?: OperationConfig
}
/**
@ -238,7 +244,8 @@ export async function createStorage(
accessKeyId: options.s3Storage.accessKeyId,
secretAccessKey: options.s3Storage.secretAccessKey,
sessionToken: options.s3Storage.sessionToken,
serviceType: 's3'
serviceType: 's3',
operationConfig: options.operationConfig
})
} else {
console.warn('S3 storage configuration is missing, falling back to memory storage')