BREAKING CHANGE(core): Refactor to v3: introduce modular core/domain architecture, plugin system, observability and strict TypeScript configuration; remove legacy classes
This commit is contained in:
31
ts/domain/transactions/index.ts
Normal file
31
ts/domain/transactions/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Transaction Module
|
||||
*
|
||||
* Distributed transactions with ACID-like semantics
|
||||
*/
|
||||
|
||||
// Main classes
|
||||
export {
|
||||
TransactionManager,
|
||||
Transaction,
|
||||
createTransactionManager,
|
||||
} from './transaction-manager.js';
|
||||
|
||||
// Types
|
||||
export type {
|
||||
TransactionIsolationLevel,
|
||||
TransactionState,
|
||||
LockingStrategy,
|
||||
TransactionOperationType,
|
||||
TransactionOperation,
|
||||
TransactionConfig,
|
||||
TransactionContext,
|
||||
TransactionResult,
|
||||
TransactionStats,
|
||||
LockInfo,
|
||||
ConflictResolutionStrategy,
|
||||
ConflictInfo,
|
||||
TransactionManagerConfig,
|
||||
Savepoint,
|
||||
TransactionCallbacks,
|
||||
} from './types.js';
|
||||
859
ts/domain/transactions/transaction-manager.ts
Normal file
859
ts/domain/transactions/transaction-manager.ts
Normal file
@@ -0,0 +1,859 @@
|
||||
/**
|
||||
* Transaction Manager
|
||||
*
|
||||
* Manages distributed transactions with ACID-like semantics
|
||||
*/
|
||||
|
||||
import { ElasticsearchConnectionManager } from '../../core/connection/connection-manager.js';
|
||||
import { Logger, defaultLogger } from '../../core/observability/logger.js';
|
||||
import { MetricsCollector, defaultMetricsCollector } from '../../core/observability/metrics.js';
|
||||
import { DocumentConflictError } from '../../core/errors/index.js';
|
||||
import type {
|
||||
TransactionConfig,
|
||||
TransactionContext,
|
||||
TransactionOperation,
|
||||
TransactionResult,
|
||||
TransactionStats,
|
||||
TransactionState,
|
||||
TransactionManagerConfig,
|
||||
TransactionCallbacks,
|
||||
ConflictInfo,
|
||||
ConflictResolutionStrategy,
|
||||
Savepoint,
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* Default configuration
|
||||
*/
|
||||
const DEFAULT_CONFIG: Required<TransactionManagerConfig> = {
|
||||
defaultIsolationLevel: 'read_committed',
|
||||
defaultLockingStrategy: 'optimistic',
|
||||
defaultTimeout: 30000, // 30 seconds
|
||||
enableCleanup: true,
|
||||
cleanupInterval: 60000, // 1 minute
|
||||
maxConcurrentTransactions: 1000,
|
||||
conflictResolution: 'retry',
|
||||
enableLogging: true,
|
||||
enableMetrics: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Transaction Manager
|
||||
*/
|
||||
export class TransactionManager {
|
||||
private config: Required<TransactionManagerConfig>;
|
||||
private transactions: Map<string, TransactionContext> = new Map();
|
||||
private stats: TransactionStats;
|
||||
private cleanupTimer?: NodeJS.Timeout;
|
||||
private logger: Logger;
|
||||
private metrics: MetricsCollector;
|
||||
private transactionCounter = 0;
|
||||
|
||||
constructor(config: TransactionManagerConfig = {}) {
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
this.logger = defaultLogger;
|
||||
this.metrics = defaultMetricsCollector;
|
||||
|
||||
this.stats = {
|
||||
totalStarted: 0,
|
||||
totalCommitted: 0,
|
||||
totalRolledBack: 0,
|
||||
totalFailed: 0,
|
||||
totalOperations: 0,
|
||||
totalConflicts: 0,
|
||||
totalRetries: 0,
|
||||
avgDuration: 0,
|
||||
avgOperationsPerTransaction: 0,
|
||||
successRate: 0,
|
||||
activeTransactions: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize transaction manager
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.config.enableCleanup) {
|
||||
this.startCleanupTimer();
|
||||
}
|
||||
|
||||
this.logger.info('TransactionManager initialized', {
|
||||
defaultIsolationLevel: this.config.defaultIsolationLevel,
|
||||
defaultLockingStrategy: this.config.defaultLockingStrategy,
|
||||
maxConcurrentTransactions: this.config.maxConcurrentTransactions,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin a new transaction
|
||||
*/
|
||||
async begin(
|
||||
config: TransactionConfig = {},
|
||||
callbacks?: TransactionCallbacks
|
||||
): Promise<Transaction> {
|
||||
// Check concurrent transaction limit
|
||||
if (this.transactions.size >= this.config.maxConcurrentTransactions) {
|
||||
throw new Error(
|
||||
`Maximum concurrent transactions limit reached (${this.config.maxConcurrentTransactions})`
|
||||
);
|
||||
}
|
||||
|
||||
// Generate transaction ID
|
||||
const transactionId = config.id || this.generateTransactionId();
|
||||
|
||||
// Create transaction context
|
||||
const context: TransactionContext = {
|
||||
id: transactionId,
|
||||
state: 'active',
|
||||
config: {
|
||||
id: transactionId,
|
||||
isolationLevel: config.isolationLevel ?? this.config.defaultIsolationLevel,
|
||||
lockingStrategy: config.lockingStrategy ?? this.config.defaultLockingStrategy,
|
||||
timeout: config.timeout ?? this.config.defaultTimeout,
|
||||
autoRollback: config.autoRollback ?? true,
|
||||
maxRetries: config.maxRetries ?? 3,
|
||||
retryDelay: config.retryDelay ?? 100,
|
||||
strictOrdering: config.strictOrdering ?? false,
|
||||
metadata: config.metadata ?? {},
|
||||
},
|
||||
operations: [],
|
||||
readSet: new Map(),
|
||||
writeSet: new Set(),
|
||||
startTime: new Date(),
|
||||
retryAttempts: 0,
|
||||
};
|
||||
|
||||
this.transactions.set(transactionId, context);
|
||||
this.stats.totalStarted++;
|
||||
this.stats.activeTransactions++;
|
||||
|
||||
if (this.config.enableLogging) {
|
||||
this.logger.info('Transaction started', {
|
||||
transactionId,
|
||||
isolationLevel: context.config.isolationLevel,
|
||||
lockingStrategy: context.config.lockingStrategy,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.config.enableMetrics) {
|
||||
this.metrics.recordCounter('transactions.started', 1);
|
||||
this.metrics.recordGauge('transactions.active', this.stats.activeTransactions);
|
||||
}
|
||||
|
||||
// Call onBegin callback
|
||||
if (callbacks?.onBegin) {
|
||||
await callbacks.onBegin(context);
|
||||
}
|
||||
|
||||
return new Transaction(this, context, callbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction context
|
||||
*/
|
||||
getTransaction(transactionId: string): TransactionContext | undefined {
|
||||
return this.transactions.get(transactionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit a transaction
|
||||
*/
|
||||
async commit(transactionId: string, callbacks?: TransactionCallbacks): Promise<TransactionResult> {
|
||||
const context = this.transactions.get(transactionId);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(`Transaction ${transactionId} not found`);
|
||||
}
|
||||
|
||||
if (context.state !== 'active' && context.state !== 'prepared') {
|
||||
throw new Error(`Cannot commit transaction in state: ${context.state}`);
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Call onBeforeCommit callback
|
||||
if (callbacks?.onBeforeCommit) {
|
||||
await callbacks.onBeforeCommit(context);
|
||||
}
|
||||
|
||||
context.state = 'committing';
|
||||
|
||||
const client = ElasticsearchConnectionManager.getInstance().getClient();
|
||||
|
||||
// Execute and commit all operations
|
||||
let committed = 0;
|
||||
for (const operation of context.operations) {
|
||||
if (operation.committed) {
|
||||
committed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Execute operation if not yet executed
|
||||
if (!operation.executed) {
|
||||
await this.executeOperation(context, operation, callbacks);
|
||||
}
|
||||
|
||||
// Mark as committed
|
||||
operation.committed = true;
|
||||
committed++;
|
||||
}
|
||||
|
||||
context.state = 'committed';
|
||||
context.endTime = new Date();
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.stats.totalCommitted++;
|
||||
this.stats.activeTransactions--;
|
||||
this.updateAverages(duration, context.operations.length);
|
||||
|
||||
const result: TransactionResult = {
|
||||
success: true,
|
||||
transactionId,
|
||||
state: 'committed',
|
||||
operationsExecuted: context.operations.filter((op) => op.executed).length,
|
||||
operationsCommitted: committed,
|
||||
operationsRolledBack: 0,
|
||||
duration,
|
||||
metadata: context.config.metadata,
|
||||
};
|
||||
|
||||
if (this.config.enableLogging) {
|
||||
this.logger.info('Transaction committed', {
|
||||
transactionId,
|
||||
operations: committed,
|
||||
duration,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.config.enableMetrics) {
|
||||
this.metrics.recordCounter('transactions.committed', 1);
|
||||
this.metrics.recordHistogram('transactions.duration', duration);
|
||||
this.metrics.recordGauge('transactions.active', this.stats.activeTransactions);
|
||||
}
|
||||
|
||||
// Call onAfterCommit callback
|
||||
if (callbacks?.onAfterCommit) {
|
||||
await callbacks.onAfterCommit(result);
|
||||
}
|
||||
|
||||
// Cleanup transaction
|
||||
this.transactions.delete(transactionId);
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
context.state = 'failed';
|
||||
context.error = error;
|
||||
|
||||
if (this.config.enableLogging) {
|
||||
this.logger.error('Transaction commit failed', {
|
||||
transactionId,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-rollback if enabled
|
||||
if (context.config.autoRollback) {
|
||||
return await this.rollback(transactionId, callbacks);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback a transaction
|
||||
*/
|
||||
async rollback(
|
||||
transactionId: string,
|
||||
callbacks?: TransactionCallbacks
|
||||
): Promise<TransactionResult> {
|
||||
const context = this.transactions.get(transactionId);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(`Transaction ${transactionId} not found`);
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Call onBeforeRollback callback
|
||||
if (callbacks?.onBeforeRollback) {
|
||||
await callbacks.onBeforeRollback(context);
|
||||
}
|
||||
|
||||
context.state = 'rolling_back';
|
||||
|
||||
const client = ElasticsearchConnectionManager.getInstance().getClient();
|
||||
|
||||
// Execute compensation operations in reverse order
|
||||
let rolledBack = 0;
|
||||
for (let i = context.operations.length - 1; i >= 0; i--) {
|
||||
const operation = context.operations[i];
|
||||
|
||||
if (!operation.executed || !operation.compensation) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.executeOperation(context, operation.compensation);
|
||||
rolledBack++;
|
||||
} catch (error: any) {
|
||||
this.logger.error('Compensation operation failed', {
|
||||
transactionId,
|
||||
operation: operation.type,
|
||||
index: operation.index,
|
||||
id: operation.id,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
context.state = 'rolled_back';
|
||||
context.endTime = new Date();
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.stats.totalRolledBack++;
|
||||
this.stats.activeTransactions--;
|
||||
|
||||
const result: TransactionResult = {
|
||||
success: false,
|
||||
transactionId,
|
||||
state: 'rolled_back',
|
||||
operationsExecuted: context.operations.filter((op) => op.executed).length,
|
||||
operationsCommitted: 0,
|
||||
operationsRolledBack: rolledBack,
|
||||
duration,
|
||||
error: context.error
|
||||
? {
|
||||
message: context.error.message,
|
||||
type: context.error.name,
|
||||
}
|
||||
: undefined,
|
||||
metadata: context.config.metadata,
|
||||
};
|
||||
|
||||
if (this.config.enableLogging) {
|
||||
this.logger.info('Transaction rolled back', {
|
||||
transactionId,
|
||||
rolledBack,
|
||||
duration,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.config.enableMetrics) {
|
||||
this.metrics.recordCounter('transactions.rolled_back', 1);
|
||||
this.metrics.recordGauge('transactions.active', this.stats.activeTransactions);
|
||||
}
|
||||
|
||||
// Call onAfterRollback callback
|
||||
if (callbacks?.onAfterRollback) {
|
||||
await callbacks.onAfterRollback(result);
|
||||
}
|
||||
|
||||
// Cleanup transaction
|
||||
this.transactions.delete(transactionId);
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
context.state = 'failed';
|
||||
context.error = error;
|
||||
this.stats.totalFailed++;
|
||||
|
||||
if (this.config.enableLogging) {
|
||||
this.logger.error('Transaction rollback failed', {
|
||||
transactionId,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction statistics
|
||||
*/
|
||||
getStats(): TransactionStats {
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy transaction manager
|
||||
*/
|
||||
async destroy(): Promise<void> {
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
}
|
||||
|
||||
// Rollback all active transactions
|
||||
const activeTransactions = Array.from(this.transactions.keys());
|
||||
for (const transactionId of activeTransactions) {
|
||||
try {
|
||||
await this.rollback(transactionId);
|
||||
} catch (error) {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
}
|
||||
|
||||
this.transactions.clear();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Internal Methods
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Add operation to transaction
|
||||
*/
|
||||
addOperation(context: TransactionContext, operation: TransactionOperation): void {
|
||||
context.operations.push(operation);
|
||||
this.stats.totalOperations++;
|
||||
|
||||
const key = `${operation.index}:${operation.id}`;
|
||||
|
||||
if (operation.type === 'read') {
|
||||
// Add to read set for repeatable read
|
||||
if (operation.version) {
|
||||
context.readSet.set(key, operation.version);
|
||||
}
|
||||
} else {
|
||||
// Add to write set
|
||||
context.writeSet.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an operation
|
||||
*/
|
||||
private async executeOperation(
|
||||
context: TransactionContext,
|
||||
operation: TransactionOperation,
|
||||
callbacks?: TransactionCallbacks
|
||||
): Promise<void> {
|
||||
// Call onBeforeOperation callback
|
||||
if (callbacks?.onBeforeOperation) {
|
||||
await callbacks.onBeforeOperation(operation);
|
||||
}
|
||||
|
||||
const client = ElasticsearchConnectionManager.getInstance().getClient();
|
||||
|
||||
try {
|
||||
switch (operation.type) {
|
||||
case 'read': {
|
||||
const result = await client.get({
|
||||
index: operation.index,
|
||||
id: operation.id,
|
||||
});
|
||||
|
||||
operation.version = {
|
||||
seqNo: result._seq_no!,
|
||||
primaryTerm: result._primary_term!,
|
||||
};
|
||||
|
||||
operation.originalDocument = result._source;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'create': {
|
||||
const result = await client.index({
|
||||
index: operation.index,
|
||||
id: operation.id,
|
||||
document: operation.document,
|
||||
op_type: 'create',
|
||||
});
|
||||
|
||||
operation.version = {
|
||||
seqNo: result._seq_no,
|
||||
primaryTerm: result._primary_term,
|
||||
};
|
||||
|
||||
// Create compensation (delete)
|
||||
operation.compensation = {
|
||||
type: 'delete',
|
||||
index: operation.index,
|
||||
id: operation.id,
|
||||
timestamp: new Date(),
|
||||
executed: false,
|
||||
committed: false,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
case 'update': {
|
||||
const updateRequest: any = {
|
||||
index: operation.index,
|
||||
id: operation.id,
|
||||
document: operation.document,
|
||||
};
|
||||
|
||||
// Add version for optimistic locking
|
||||
if (operation.version) {
|
||||
updateRequest.if_seq_no = operation.version.seqNo;
|
||||
updateRequest.if_primary_term = operation.version.primaryTerm;
|
||||
}
|
||||
|
||||
const result = await client.index(updateRequest);
|
||||
|
||||
operation.version = {
|
||||
seqNo: result._seq_no,
|
||||
primaryTerm: result._primary_term,
|
||||
};
|
||||
|
||||
// Create compensation (restore original)
|
||||
if (operation.originalDocument) {
|
||||
operation.compensation = {
|
||||
type: 'update',
|
||||
index: operation.index,
|
||||
id: operation.id,
|
||||
document: operation.originalDocument,
|
||||
timestamp: new Date(),
|
||||
executed: false,
|
||||
committed: false,
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'delete': {
|
||||
const deleteRequest: any = {
|
||||
index: operation.index,
|
||||
id: operation.id,
|
||||
};
|
||||
|
||||
// Add version for optimistic locking
|
||||
if (operation.version) {
|
||||
deleteRequest.if_seq_no = operation.version.seqNo;
|
||||
deleteRequest.if_primary_term = operation.version.primaryTerm;
|
||||
}
|
||||
|
||||
await client.delete(deleteRequest);
|
||||
|
||||
// Create compensation (restore document)
|
||||
if (operation.originalDocument) {
|
||||
operation.compensation = {
|
||||
type: 'create',
|
||||
index: operation.index,
|
||||
id: operation.id,
|
||||
document: operation.originalDocument,
|
||||
timestamp: new Date(),
|
||||
executed: false,
|
||||
committed: false,
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
operation.executed = true;
|
||||
|
||||
// Call onAfterOperation callback
|
||||
if (callbacks?.onAfterOperation) {
|
||||
await callbacks.onAfterOperation(operation);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Handle version conflict
|
||||
if (error.name === 'ResponseError' && error.meta?.statusCode === 409) {
|
||||
await this.handleConflict(context, operation, error, callbacks);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle version conflict
|
||||
*/
|
||||
private async handleConflict(
|
||||
context: TransactionContext,
|
||||
operation: TransactionOperation,
|
||||
error: Error,
|
||||
callbacks?: TransactionCallbacks
|
||||
): Promise<void> {
|
||||
this.stats.totalConflicts++;
|
||||
|
||||
const conflict: ConflictInfo = {
|
||||
operation,
|
||||
expectedVersion: operation.version,
|
||||
detectedAt: new Date(),
|
||||
};
|
||||
|
||||
if (this.config.enableMetrics) {
|
||||
this.metrics.recordCounter('transactions.conflicts', 1);
|
||||
}
|
||||
|
||||
// Call onConflict callback
|
||||
let strategy: ConflictResolutionStrategy = this.config.conflictResolution;
|
||||
if (callbacks?.onConflict) {
|
||||
strategy = await callbacks.onConflict(conflict);
|
||||
}
|
||||
|
||||
switch (strategy) {
|
||||
case 'abort':
|
||||
throw new DocumentConflictError(
|
||||
`Version conflict for ${operation.index}/${operation.id}`,
|
||||
{ index: operation.index, id: operation.id }
|
||||
);
|
||||
|
||||
case 'retry':
|
||||
if (context.retryAttempts >= context.config.maxRetries) {
|
||||
throw new DocumentConflictError(
|
||||
`Max retries exceeded for ${operation.index}/${operation.id}`,
|
||||
{ index: operation.index, id: operation.id }
|
||||
);
|
||||
}
|
||||
|
||||
context.retryAttempts++;
|
||||
this.stats.totalRetries++;
|
||||
|
||||
// Wait before retry
|
||||
await new Promise((resolve) => setTimeout(resolve, context.config.retryDelay));
|
||||
|
||||
// Retry operation
|
||||
await this.executeOperation(context, operation, callbacks);
|
||||
break;
|
||||
|
||||
case 'skip':
|
||||
// Skip this operation
|
||||
operation.executed = false;
|
||||
break;
|
||||
|
||||
case 'force':
|
||||
// Force update without version check
|
||||
delete operation.version;
|
||||
await this.executeOperation(context, operation, callbacks);
|
||||
break;
|
||||
|
||||
case 'merge':
|
||||
// Not implemented - requires custom merge logic
|
||||
throw new Error('Merge conflict resolution not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate transaction ID
|
||||
*/
|
||||
private generateTransactionId(): string {
|
||||
this.transactionCounter++;
|
||||
return `txn-${Date.now()}-${this.transactionCounter}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start cleanup timer for expired transactions
|
||||
*/
|
||||
private startCleanupTimer(): void {
|
||||
this.cleanupTimer = setInterval(() => {
|
||||
this.cleanupExpiredTransactions();
|
||||
}, this.config.cleanupInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup expired transactions
|
||||
*/
|
||||
private cleanupExpiredTransactions(): void {
|
||||
const now = Date.now();
|
||||
|
||||
for (const [transactionId, context] of this.transactions) {
|
||||
const elapsed = now - context.startTime.getTime();
|
||||
|
||||
if (elapsed > context.config.timeout) {
|
||||
this.logger.warn('Transaction timeout, rolling back', { transactionId });
|
||||
|
||||
this.rollback(transactionId).catch((error) => {
|
||||
this.logger.error('Failed to rollback expired transaction', {
|
||||
transactionId,
|
||||
error,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update average statistics
|
||||
*/
|
||||
private updateAverages(duration: number, operations: number): void {
|
||||
const total = this.stats.totalCommitted + this.stats.totalRolledBack;
|
||||
|
||||
this.stats.avgDuration =
|
||||
(this.stats.avgDuration * (total - 1) + duration) / total;
|
||||
|
||||
this.stats.avgOperationsPerTransaction =
|
||||
(this.stats.avgOperationsPerTransaction * (total - 1) + operations) / total;
|
||||
|
||||
this.stats.successRate =
|
||||
this.stats.totalCommitted / (this.stats.totalCommitted + this.stats.totalRolledBack + this.stats.totalFailed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction class for fluent API
|
||||
*/
|
||||
export class Transaction {
|
||||
private savepoints: Map<string, Savepoint> = new Map();
|
||||
|
||||
constructor(
|
||||
private manager: TransactionManager,
|
||||
private context: TransactionContext,
|
||||
private callbacks?: TransactionCallbacks
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get transaction ID
|
||||
*/
|
||||
getId(): string {
|
||||
return this.context.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction state
|
||||
*/
|
||||
getState(): TransactionState {
|
||||
return this.context.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a document
|
||||
*/
|
||||
async read<T>(index: string, id: string): Promise<T | null> {
|
||||
const operation: TransactionOperation<T> = {
|
||||
type: 'read',
|
||||
index,
|
||||
id,
|
||||
timestamp: new Date(),
|
||||
executed: false,
|
||||
committed: false,
|
||||
};
|
||||
|
||||
this.manager.addOperation(this.context, operation);
|
||||
|
||||
const client = ElasticsearchConnectionManager.getInstance().getClient();
|
||||
|
||||
try {
|
||||
const result = await client.get({ index, id });
|
||||
|
||||
operation.version = {
|
||||
seqNo: result._seq_no!,
|
||||
primaryTerm: result._primary_term!,
|
||||
};
|
||||
|
||||
operation.originalDocument = result._source as T;
|
||||
operation.executed = true;
|
||||
|
||||
return result._source as T;
|
||||
} catch (error: any) {
|
||||
if (error.name === 'ResponseError' && error.meta?.statusCode === 404) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a document
|
||||
*/
|
||||
async create<T>(index: string, id: string, document: T): Promise<void> {
|
||||
const operation: TransactionOperation<T> = {
|
||||
type: 'create',
|
||||
index,
|
||||
id,
|
||||
document,
|
||||
timestamp: new Date(),
|
||||
executed: false,
|
||||
committed: false,
|
||||
};
|
||||
|
||||
this.manager.addOperation(this.context, operation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a document
|
||||
*/
|
||||
async update<T>(index: string, id: string, document: Partial<T>): Promise<void> {
|
||||
// First read the current version
|
||||
const current = await this.read<T>(index, id);
|
||||
|
||||
const operation: TransactionOperation<T> = {
|
||||
type: 'update',
|
||||
index,
|
||||
id,
|
||||
document: { ...current, ...document } as T,
|
||||
originalDocument: current ?? undefined,
|
||||
timestamp: new Date(),
|
||||
executed: false,
|
||||
committed: false,
|
||||
};
|
||||
|
||||
this.manager.addOperation(this.context, operation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a document
|
||||
*/
|
||||
async delete(index: string, id: string): Promise<void> {
|
||||
// First read the current version
|
||||
const current = await this.read(index, id);
|
||||
|
||||
const operation: TransactionOperation = {
|
||||
type: 'delete',
|
||||
index,
|
||||
id,
|
||||
originalDocument: current ?? undefined,
|
||||
timestamp: new Date(),
|
||||
executed: false,
|
||||
committed: false,
|
||||
};
|
||||
|
||||
this.manager.addOperation(this.context, operation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a savepoint
|
||||
*/
|
||||
savepoint(name: string): void {
|
||||
this.savepoints.set(name, {
|
||||
name,
|
||||
operationsCount: this.context.operations.length,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback to savepoint
|
||||
*/
|
||||
rollbackTo(name: string): void {
|
||||
const savepoint = this.savepoints.get(name);
|
||||
|
||||
if (!savepoint) {
|
||||
throw new Error(`Savepoint '${name}' not found`);
|
||||
}
|
||||
|
||||
// Remove operations after savepoint
|
||||
this.context.operations.splice(savepoint.operationsCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit the transaction
|
||||
*/
|
||||
async commit(): Promise<TransactionResult> {
|
||||
return await this.manager.commit(this.context.id, this.callbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback the transaction
|
||||
*/
|
||||
async rollback(): Promise<TransactionResult> {
|
||||
return await this.manager.rollback(this.context.id, this.callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a transaction manager
|
||||
*/
|
||||
export function createTransactionManager(
|
||||
config?: TransactionManagerConfig
|
||||
): TransactionManager {
|
||||
return new TransactionManager(config);
|
||||
}
|
||||
361
ts/domain/transactions/types.ts
Normal file
361
ts/domain/transactions/types.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* Transaction types for distributed ACID-like operations
|
||||
*
|
||||
* Note: Elasticsearch doesn't natively support ACID transactions across multiple
|
||||
* documents. This implementation provides transaction-like semantics using:
|
||||
* - Optimistic concurrency control (seq_no/primary_term)
|
||||
* - Two-phase operations (prepare/commit)
|
||||
* - Compensation-based rollback
|
||||
* - Transaction state tracking
|
||||
*/
|
||||
|
||||
/**
|
||||
* Transaction isolation level
|
||||
*/
|
||||
export type TransactionIsolationLevel =
|
||||
| 'read_uncommitted'
|
||||
| 'read_committed'
|
||||
| 'repeatable_read'
|
||||
| 'serializable';
|
||||
|
||||
/**
|
||||
* Transaction state
|
||||
*/
|
||||
export type TransactionState =
|
||||
| 'active'
|
||||
| 'preparing'
|
||||
| 'prepared'
|
||||
| 'committing'
|
||||
| 'committed'
|
||||
| 'rolling_back'
|
||||
| 'rolled_back'
|
||||
| 'failed';
|
||||
|
||||
/**
|
||||
* Transaction locking strategy
|
||||
*/
|
||||
export type LockingStrategy = 'optimistic' | 'pessimistic';
|
||||
|
||||
/**
|
||||
* Transaction operation type
|
||||
*/
|
||||
export type TransactionOperationType = 'read' | 'create' | 'update' | 'delete';
|
||||
|
||||
/**
|
||||
* Transaction operation
|
||||
*/
|
||||
export interface TransactionOperation<T = unknown> {
|
||||
/** Operation type */
|
||||
type: TransactionOperationType;
|
||||
|
||||
/** Target index */
|
||||
index: string;
|
||||
|
||||
/** Document ID */
|
||||
id: string;
|
||||
|
||||
/** Document data (for create/update) */
|
||||
document?: T;
|
||||
|
||||
/** Original document (for rollback) */
|
||||
originalDocument?: T;
|
||||
|
||||
/** Version info for optimistic locking */
|
||||
version?: {
|
||||
seqNo: number;
|
||||
primaryTerm: number;
|
||||
};
|
||||
|
||||
/** Timestamp when operation was added */
|
||||
timestamp: Date;
|
||||
|
||||
/** Whether operation has been executed */
|
||||
executed: boolean;
|
||||
|
||||
/** Whether operation has been committed */
|
||||
committed: boolean;
|
||||
|
||||
/** Compensation operation for rollback */
|
||||
compensation?: TransactionOperation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction configuration
|
||||
*/
|
||||
export interface TransactionConfig {
|
||||
/** Transaction ID (auto-generated if not provided) */
|
||||
id?: string;
|
||||
|
||||
/** Isolation level */
|
||||
isolationLevel?: TransactionIsolationLevel;
|
||||
|
||||
/** Locking strategy */
|
||||
lockingStrategy?: LockingStrategy;
|
||||
|
||||
/** Transaction timeout in milliseconds */
|
||||
timeout?: number;
|
||||
|
||||
/** Enable automatic rollback on error */
|
||||
autoRollback?: boolean;
|
||||
|
||||
/** Maximum retry attempts for conflicts */
|
||||
maxRetries?: number;
|
||||
|
||||
/** Retry delay in milliseconds */
|
||||
retryDelay?: number;
|
||||
|
||||
/** Enable strict ordering of operations */
|
||||
strictOrdering?: boolean;
|
||||
|
||||
/** Metadata for tracking */
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction context
|
||||
*/
|
||||
export interface TransactionContext {
|
||||
/** Transaction ID */
|
||||
id: string;
|
||||
|
||||
/** Current state */
|
||||
state: TransactionState;
|
||||
|
||||
/** Configuration */
|
||||
config: Required<TransactionConfig>;
|
||||
|
||||
/** Operations in this transaction */
|
||||
operations: TransactionOperation[];
|
||||
|
||||
/** Read set (for repeatable read isolation) */
|
||||
readSet: Map<string, { seqNo: number; primaryTerm: number }>;
|
||||
|
||||
/** Write set (for conflict detection) */
|
||||
writeSet: Set<string>;
|
||||
|
||||
/** Transaction start time */
|
||||
startTime: Date;
|
||||
|
||||
/** Transaction end time */
|
||||
endTime?: Date;
|
||||
|
||||
/** Error if transaction failed */
|
||||
error?: Error;
|
||||
|
||||
/** Number of retry attempts */
|
||||
retryAttempts: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction result
|
||||
*/
|
||||
export interface TransactionResult {
|
||||
/** Whether transaction succeeded */
|
||||
success: boolean;
|
||||
|
||||
/** Transaction ID */
|
||||
transactionId: string;
|
||||
|
||||
/** Final state */
|
||||
state: TransactionState;
|
||||
|
||||
/** Number of operations executed */
|
||||
operationsExecuted: number;
|
||||
|
||||
/** Number of operations committed */
|
||||
operationsCommitted: number;
|
||||
|
||||
/** Number of operations rolled back */
|
||||
operationsRolledBack: number;
|
||||
|
||||
/** Transaction duration in milliseconds */
|
||||
duration: number;
|
||||
|
||||
/** Error if transaction failed */
|
||||
error?: {
|
||||
message: string;
|
||||
type: string;
|
||||
operation?: TransactionOperation;
|
||||
};
|
||||
|
||||
/** Metadata */
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction statistics
|
||||
*/
|
||||
export interface TransactionStats {
|
||||
/** Total transactions started */
|
||||
totalStarted: number;
|
||||
|
||||
/** Total transactions committed */
|
||||
totalCommitted: number;
|
||||
|
||||
/** Total transactions rolled back */
|
||||
totalRolledBack: number;
|
||||
|
||||
/** Total transactions failed */
|
||||
totalFailed: number;
|
||||
|
||||
/** Total operations executed */
|
||||
totalOperations: number;
|
||||
|
||||
/** Total conflicts encountered */
|
||||
totalConflicts: number;
|
||||
|
||||
/** Total retries */
|
||||
totalRetries: number;
|
||||
|
||||
/** Average transaction duration */
|
||||
avgDuration: number;
|
||||
|
||||
/** Average operations per transaction */
|
||||
avgOperationsPerTransaction: number;
|
||||
|
||||
/** Success rate */
|
||||
successRate: number;
|
||||
|
||||
/** Active transactions count */
|
||||
activeTransactions: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock information
|
||||
*/
|
||||
export interface LockInfo {
|
||||
/** Document key (index:id) */
|
||||
key: string;
|
||||
|
||||
/** Transaction ID holding the lock */
|
||||
transactionId: string;
|
||||
|
||||
/** Lock type */
|
||||
type: 'read' | 'write';
|
||||
|
||||
/** Lock acquired at */
|
||||
acquiredAt: Date;
|
||||
|
||||
/** Lock expires at */
|
||||
expiresAt: Date;
|
||||
|
||||
/** Lock metadata */
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conflict resolution strategy
|
||||
*/
|
||||
export type ConflictResolutionStrategy =
|
||||
| 'abort' // Abort transaction
|
||||
| 'retry' // Retry operation
|
||||
| 'skip' // Skip conflicting operation
|
||||
| 'force' // Force operation (last write wins)
|
||||
| 'merge'; // Attempt to merge changes
|
||||
|
||||
/**
|
||||
* Conflict information
|
||||
*/
|
||||
export interface ConflictInfo {
|
||||
/** Operation that conflicted */
|
||||
operation: TransactionOperation;
|
||||
|
||||
/** Conflicting transaction ID */
|
||||
conflictingTransactionId?: string;
|
||||
|
||||
/** Expected version */
|
||||
expectedVersion?: {
|
||||
seqNo: number;
|
||||
primaryTerm: number;
|
||||
};
|
||||
|
||||
/** Actual version */
|
||||
actualVersion?: {
|
||||
seqNo: number;
|
||||
primaryTerm: number;
|
||||
};
|
||||
|
||||
/** Conflict detected at */
|
||||
detectedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction manager configuration
|
||||
*/
|
||||
export interface TransactionManagerConfig {
|
||||
/** Default isolation level */
|
||||
defaultIsolationLevel?: TransactionIsolationLevel;
|
||||
|
||||
/** Default locking strategy */
|
||||
defaultLockingStrategy?: LockingStrategy;
|
||||
|
||||
/** Default transaction timeout */
|
||||
defaultTimeout?: number;
|
||||
|
||||
/** Enable automatic cleanup of expired transactions */
|
||||
enableCleanup?: boolean;
|
||||
|
||||
/** Cleanup interval in milliseconds */
|
||||
cleanupInterval?: number;
|
||||
|
||||
/** Maximum concurrent transactions */
|
||||
maxConcurrentTransactions?: number;
|
||||
|
||||
/** Conflict resolution strategy */
|
||||
conflictResolution?: ConflictResolutionStrategy;
|
||||
|
||||
/** Enable transaction logging */
|
||||
enableLogging?: boolean;
|
||||
|
||||
/** Enable transaction metrics */
|
||||
enableMetrics?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Savepoint for nested transactions
|
||||
*/
|
||||
export interface Savepoint {
|
||||
/** Savepoint name */
|
||||
name: string;
|
||||
|
||||
/** Operations count at savepoint */
|
||||
operationsCount: number;
|
||||
|
||||
/** Created at */
|
||||
createdAt: Date;
|
||||
|
||||
/** Metadata */
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction callback functions
|
||||
*/
|
||||
export interface TransactionCallbacks {
|
||||
/** Called before transaction begins */
|
||||
onBegin?: (context: TransactionContext) => Promise<void> | void;
|
||||
|
||||
/** Called before operation executes */
|
||||
onBeforeOperation?: (operation: TransactionOperation) => Promise<void> | void;
|
||||
|
||||
/** Called after operation executes */
|
||||
onAfterOperation?: (operation: TransactionOperation) => Promise<void> | void;
|
||||
|
||||
/** Called on conflict */
|
||||
onConflict?: (conflict: ConflictInfo) => Promise<ConflictResolutionStrategy> | ConflictResolutionStrategy;
|
||||
|
||||
/** Called before commit */
|
||||
onBeforeCommit?: (context: TransactionContext) => Promise<void> | void;
|
||||
|
||||
/** Called after commit */
|
||||
onAfterCommit?: (result: TransactionResult) => Promise<void> | void;
|
||||
|
||||
/** Called before rollback */
|
||||
onBeforeRollback?: (context: TransactionContext) => Promise<void> | void;
|
||||
|
||||
/** Called after rollback */
|
||||
onAfterRollback?: (result: TransactionResult) => Promise<void> | void;
|
||||
|
||||
/** Called on transaction error */
|
||||
onError?: (error: Error, context: TransactionContext) => Promise<void> | void;
|
||||
}
|
||||
Reference in New Issue
Block a user