claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
773 lines (772 loc) • 29.9 kB
JavaScript
/**
* Cross-Database Transaction Manager
*
* Manages atomic transactions across Redis, SQLite, and PostgreSQL with:
* - Savepoint support for nested transactions
* - Distributed locking integration
* - Transaction timeout handling
* - Isolation level support
*
* Part of Task 3.1: Cross-Database Transaction Framework
*/ import { randomUUID } from 'crypto';
import { DatabaseErrorCode, createDatabaseError } from './errors.js';
import { createLogger } from '../logging.js';
import { generateCorrelationId } from '../correlation.js';
const logger = createLogger('transaction-manager');
/**
* Transaction isolation levels
*/ export var IsolationLevel = /*#__PURE__*/ function(IsolationLevel) {
IsolationLevel["READ_UNCOMMITTED"] = "READ UNCOMMITTED";
IsolationLevel["READ_COMMITTED"] = "READ COMMITTED";
IsolationLevel["REPEATABLE_READ"] = "REPEATABLE READ";
IsolationLevel["SERIALIZABLE"] = "SERIALIZABLE";
return IsolationLevel;
}({});
/**
* Transaction state for two-phase commit
*/ export var TransactionState = /*#__PURE__*/ function(TransactionState) {
TransactionState["ACTIVE"] = "ACTIVE";
TransactionState["PREPARING"] = "PREPARING";
TransactionState["PREPARED"] = "PREPARED";
TransactionState["COMMITTING"] = "COMMITTING";
TransactionState["COMMITTED"] = "COMMITTED";
TransactionState["ABORTING"] = "ABORTING";
TransactionState["ABORTED"] = "ABORTED";
TransactionState["ROLLED_BACK"] = "ROLLED_BACK";
return TransactionState;
}({});
/**
* Transaction class providing fluent API for cross-database transactions
*/ export class Transaction {
id;
startedAt;
databases;
correlationId;
options;
contexts = new Map();
adapters = new Map();
savepoints = new Map();
state = "ACTIVE";
isCommitted = false;
isRolledBack = false;
timeoutHandle;
prepareTimeoutHandle;
lockReleaser;
preparedDatabases = new Set();
twoPhaseCommitLog = [];
constructor(id, databases, adapters, options = {}){
this.id = id;
this.startedAt = new Date();
this.databases = databases;
this.adapters = adapters;
this.correlationId = options.correlationId || generateCorrelationId();
this.options = {
timeout: options.timeout ?? 30000,
isolationLevel: options.isolationLevel ?? "READ COMMITTED",
acquireLock: options.acquireLock ?? false,
lockTimeout: options.lockTimeout ?? 10000,
prepareTimeout: options.prepareTimeout ?? 5000,
useTwoPhaseCommit: options.useTwoPhaseCommit ?? databases.length > 1
};
logger.info('Transaction created', {
transactionId: this.id,
databases: this.databases,
correlationId: this.correlationId,
options: this.options,
useTwoPhaseCommit: this.options.useTwoPhaseCommit
});
}
/**
* Start the transaction on all databases
*/ async begin() {
if (this.isCommitted || this.isRolledBack) {
throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, 'Cannot begin transaction that has already completed', undefined, {
transactionId: this.id,
status: this.isCommitted ? 'committed' : 'rolled_back'
});
}
try {
// Set transaction timeout
this.timeoutHandle = setTimeout(()=>{
this.handleTimeout();
}, this.options.timeout);
// Begin transaction on each database
for (const dbType of this.databases){
const adapter = this.adapters.get(dbType);
if (!adapter) {
throw createDatabaseError(DatabaseErrorCode.VALIDATION_FAILED, `Database adapter not found: ${dbType}`, undefined, {
database: dbType
});
}
const context = await adapter.beginTransaction();
this.contexts.set(dbType, context);
logger.debug('Transaction started on database', {
transactionId: this.id,
database: dbType,
contextId: context.id
});
}
logger.info('Transaction began successfully', {
transactionId: this.id,
databases: this.databases
});
} catch (err) {
// Rollback any successful transaction starts
await this.rollback();
throw err;
}
}
/**
* Execute an operation on a specific database within this transaction
*/ async execute(database, operation) {
if (this.isCommitted) {
throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, 'Cannot execute operation on committed transaction', undefined, {
transactionId: this.id
});
}
if (this.isRolledBack) {
throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, 'Cannot execute operation on rolled back transaction', undefined, {
transactionId: this.id
});
}
const adapter = this.adapters.get(database);
if (!adapter) {
throw createDatabaseError(DatabaseErrorCode.VALIDATION_FAILED, `Database not part of this transaction: ${database}`, undefined, {
database,
transactionDatabases: this.databases
});
}
try {
logger.debug('Executing operation', {
transactionId: this.id,
database
});
const result = await operation(adapter);
logger.debug('Operation completed', {
transactionId: this.id,
database
});
return result;
} catch (err) {
logger.error('Operation failed', err, {
transactionId: this.id,
database
});
// Auto-rollback on error
await this.rollback();
throw err;
}
}
/**
* Create a savepoint for nested transaction control
*/ async savepoint(name) {
if (!name || !/^[a-zA-Z0-9_]+$/.test(name)) {
throw createDatabaseError(DatabaseErrorCode.VALIDATION_FAILED, 'Savepoint name must be alphanumeric with underscores', undefined, {
name
});
}
if (this.savepoints.has(name)) {
throw createDatabaseError(DatabaseErrorCode.VALIDATION_FAILED, `Savepoint already exists: ${name}`, undefined, {
name
});
}
// Create savepoint on all databases
for (const dbType of this.databases){
const adapter = this.adapters.get(dbType);
if (!adapter) continue;
try {
// Execute savepoint SQL (PostgreSQL and SQLite support this)
if (dbType === 'postgres' || dbType === 'sqlite') {
await adapter.raw(`SAVEPOINT ${name}`);
}
// Redis doesn't support savepoints, skip
} catch (err) {
throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, `Failed to create savepoint: ${name}`, err, {
database: dbType,
savepoint: name
});
}
}
this.savepoints.set(name, {
name,
createdAt: new Date(),
database: this.databases.join(',')
});
logger.info('Savepoint created', {
transactionId: this.id,
savepoint: name
});
}
/**
* Rollback to a specific savepoint
*/ async rollbackToSavepoint(name) {
if (!this.savepoints.has(name)) {
throw createDatabaseError(DatabaseErrorCode.VALIDATION_FAILED, `Savepoint not found: ${name}`, undefined, {
name,
availableSavepoints: Array.from(this.savepoints.keys())
});
}
// Rollback to savepoint on all databases
for (const dbType of this.databases){
const adapter = this.adapters.get(dbType);
if (!adapter) continue;
try {
if (dbType === 'postgres' || dbType === 'sqlite') {
await adapter.raw(`ROLLBACK TO SAVEPOINT ${name}`);
}
} catch (err) {
throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, `Failed to rollback to savepoint: ${name}`, err, {
database: dbType,
savepoint: name
});
}
}
// Remove this savepoint and all later ones
const savepoints = Array.from(this.savepoints.entries());
const targetIndex = savepoints.findIndex(([n])=>n === name);
for(let i = targetIndex; i < savepoints.length; i++){
this.savepoints.delete(savepoints[i][0]);
}
logger.info('Rolled back to savepoint', {
transactionId: this.id,
savepoint: name
});
}
/**
* Release a savepoint (no longer needed)
*/ async releaseSavepoint(name) {
if (!this.savepoints.has(name)) {
throw createDatabaseError(DatabaseErrorCode.VALIDATION_FAILED, `Savepoint not found: ${name}`, undefined, {
name
});
}
// Release savepoint on all databases
for (const dbType of this.databases){
const adapter = this.adapters.get(dbType);
if (!adapter) continue;
try {
if (dbType === 'postgres' || dbType === 'sqlite') {
await adapter.raw(`RELEASE SAVEPOINT ${name}`);
}
} catch (err) {
logger.warn('Failed to release savepoint (non-fatal)', {
transactionId: this.id,
database: dbType,
savepoint: name,
error: err.message
});
}
}
this.savepoints.delete(name);
logger.debug('Savepoint released', {
transactionId: this.id,
savepoint: name
});
}
/**
* Log 2PC state transition
* @private
*/ log2PCState(state, preparedDatabases = [], failedDatabases = [], error) {
const logEntry = {
transactionId: this.id,
state,
timestamp: new Date(),
databases: this.databases,
preparedDatabases,
failedDatabases,
error
};
this.twoPhaseCommitLog.push(logEntry);
logger.info('2PC state transition', {
transactionId: this.id,
from: this.state,
to: state,
preparedDatabases,
failedDatabases,
error
});
this.state = state;
}
/**
* Get 2PC transaction log
*/ get2PCLog() {
return [
...this.twoPhaseCommitLog
];
}
/**
* Get current transaction state
*/ getTransactionState() {
return this.state;
}
/**
* Phase 1: PREPARE - Validate all databases can commit
* @private
*/ async preparePhase() {
this.log2PCState("PREPARING");
const preparedDbs = [];
const failedDbs = [];
let prepareError;
// Set prepare timeout
const preparePromise = new Promise(async (resolve, reject)=>{
this.prepareTimeoutHandle = setTimeout(()=>{
reject(createDatabaseError(DatabaseErrorCode.TIMEOUT, `Prepare phase timeout after ${this.options.prepareTimeout}ms`, undefined, {
transactionId: this.id,
timeout: this.options.prepareTimeout
}));
}, this.options.prepareTimeout);
try {
// Attempt to prepare all databases
for (const [dbType, context] of Array.from(this.contexts.entries())){
const adapter = this.adapters.get(dbType);
if (!adapter) {
failedDbs.push(dbType);
continue;
}
try {
const prepared = await adapter.prepareTransaction(context);
if (prepared) {
preparedDbs.push(dbType);
this.preparedDatabases.add(dbType);
logger.debug('Database prepared successfully', {
transactionId: this.id,
database: dbType
});
} else {
failedDbs.push(dbType);
logger.warn('Database failed to prepare', {
transactionId: this.id,
database: dbType
});
}
} catch (err) {
failedDbs.push(dbType);
prepareError = err.message;
logger.error('Database prepare error', err, {
transactionId: this.id,
database: dbType
});
}
}
// Clear prepare timeout
if (this.prepareTimeoutHandle) {
clearTimeout(this.prepareTimeoutHandle);
}
const allPrepared = failedDbs.length === 0;
if (allPrepared) {
this.log2PCState("PREPARED", preparedDbs);
resolve(true);
} else {
this.log2PCState("ABORTING", preparedDbs, failedDbs, prepareError || 'One or more databases failed to prepare');
resolve(false);
}
} catch (err) {
reject(err);
}
});
try {
return await preparePromise;
} catch (err) {
// Timeout or other error
this.log2PCState("ABORTING", preparedDbs, failedDbs, err.message);
throw err;
}
}
/**
* Phase 2: COMMIT - Commit all prepared databases
* @private
*/ async commitPhase() {
this.log2PCState("COMMITTING", Array.from(this.preparedDatabases));
const errors = [];
const failedDbs = [];
// Commit all prepared databases
for (const [dbType, context] of Array.from(this.contexts.entries())){
const adapter = this.adapters.get(dbType);
if (!adapter) continue;
try {
await adapter.commitTransaction(context);
logger.debug('Transaction committed on database', {
transactionId: this.id,
database: dbType
});
} catch (err) {
errors.push(err);
failedDbs.push(dbType);
logger.error('Failed to commit on database', err, {
transactionId: this.id,
database: dbType
});
}
}
// If any commits failed after PREPARE succeeded, this is critical
if (errors.length > 0) {
this.log2PCState("COMMITTED", Array.from(this.preparedDatabases).filter((db)=>!failedDbs.includes(db)), failedDbs, `Partial commit: ${errors.length} databases failed`);
logger.error('CRITICAL: Partial commit occurred after PREPARE', new Error('Partial commit'), {
transactionId: this.id,
failedDatabases: failedDbs,
totalDatabases: this.contexts.size,
preparedDatabases: Array.from(this.preparedDatabases)
});
throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, 'Transaction partially committed - data may be inconsistent', undefined, {
transactionId: this.id,
preparedDatabases: Array.from(this.preparedDatabases),
failedDatabases: failedDbs,
errors: errors.map((e)=>e.message)
});
}
this.log2PCState("COMMITTED", Array.from(this.preparedDatabases));
}
/**
* Abort transaction (rollback all prepared databases)
* @private
*/ async abortPhase() {
this.log2PCState("ABORTING", Array.from(this.preparedDatabases));
// Rollback all databases (both prepared and not prepared)
for (const [dbType, context] of Array.from(this.contexts.entries())){
const adapter = this.adapters.get(dbType);
if (!adapter) continue;
try {
await adapter.rollbackTransaction(context);
logger.debug('Transaction rolled back on database', {
transactionId: this.id,
database: dbType
});
} catch (err) {
logger.error('Failed to rollback on database (non-fatal)', err, {
transactionId: this.id,
database: dbType
});
}
}
this.log2PCState("ABORTED", [], Array.from(this.preparedDatabases));
}
/**
* Commit the transaction using two-phase commit protocol
*/ async commit() {
if (this.isCommitted) {
logger.warn('Transaction already committed', {
transactionId: this.id
});
return;
}
if (this.isRolledBack) {
throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, 'Cannot commit rolled back transaction', undefined, {
transactionId: this.id
});
}
// Clear timeout
if (this.timeoutHandle) {
clearTimeout(this.timeoutHandle);
}
try {
// Use two-phase commit for multi-database transactions
if (this.options.useTwoPhaseCommit && this.databases.length > 1) {
logger.info('Starting two-phase commit', {
transactionId: this.id,
databases: this.databases
});
// Phase 1: PREPARE
const prepared = await this.preparePhase();
if (!prepared) {
// At least one database failed to prepare, abort all
logger.warn('Prepare phase failed, aborting transaction', {
transactionId: this.id,
preparedDatabases: Array.from(this.preparedDatabases)
});
await this.abortPhase();
throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, 'Transaction prepare phase failed - all databases rolled back', undefined, {
transactionId: this.id,
preparedDatabases: Array.from(this.preparedDatabases),
log: this.get2PCLog()
});
}
// Phase 2: COMMIT
await this.commitPhase();
this.isCommitted = true;
logger.info('Two-phase commit completed successfully', {
transactionId: this.id,
duration: Date.now() - this.startedAt.getTime(),
databases: this.databases,
log: this.get2PCLog()
});
} else {
// Single database or 2PC disabled - use legacy commit
logger.info('Using legacy commit (single database or 2PC disabled)', {
transactionId: this.id,
databases: this.databases,
use2PC: this.options.useTwoPhaseCommit
});
const errors = [];
// Commit all database transactions
for (const [dbType, context] of Array.from(this.contexts.entries())){
const adapter = this.adapters.get(dbType);
if (!adapter) continue;
try {
await adapter.commitTransaction(context);
logger.debug('Transaction committed on database', {
transactionId: this.id,
database: dbType
});
} catch (err) {
errors.push(err);
logger.error('Failed to commit on database', err, {
transactionId: this.id,
database: dbType
});
}
}
// If any commits failed, this is a partial commit - log critical error
if (errors.length > 0) {
logger.error('CRITICAL: Partial commit occurred', new Error('Partial commit'), {
transactionId: this.id,
failedDatabases: errors.length,
totalDatabases: this.contexts.size
});
throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, 'Transaction partially committed - data may be inconsistent', undefined, {
transactionId: this.id,
errors: errors.map((e)=>e.message)
});
}
this.isCommitted = true;
logger.info('Transaction committed successfully', {
transactionId: this.id,
duration: Date.now() - this.startedAt.getTime()
});
}
// Release distributed lock if acquired
await this.releaseLock();
} catch (err) {
// On any error, attempt to rollback
try {
if (this.state === "PREPARED" || this.state === "PREPARING") {
await this.abortPhase();
}
} catch (rollbackErr) {
logger.error('Failed to rollback after commit error', rollbackErr, {
transactionId: this.id
});
}
throw err;
}
}
/**
* Rollback the transaction
*/ async rollback() {
if (this.isRolledBack) {
logger.debug('Transaction already rolled back', {
transactionId: this.id
});
return;
}
if (this.isCommitted) {
throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, 'Cannot rollback committed transaction', undefined, {
transactionId: this.id
});
}
// Clear timeouts
if (this.timeoutHandle) {
clearTimeout(this.timeoutHandle);
}
if (this.prepareTimeoutHandle) {
clearTimeout(this.prepareTimeoutHandle);
}
// Update state
this.state = "ROLLED_BACK";
// Rollback all database transactions
for (const [dbType, context] of Array.from(this.contexts.entries())){
const adapter = this.adapters.get(dbType);
if (!adapter) continue;
try {
await adapter.rollbackTransaction(context);
logger.debug('Transaction rolled back on database', {
transactionId: this.id,
database: dbType
});
} catch (err) {
logger.error('Failed to rollback on database (non-fatal)', err, {
transactionId: this.id,
database: dbType
});
}
}
this.isRolledBack = true;
logger.info('Transaction rolled back', {
transactionId: this.id,
duration: Date.now() - this.startedAt.getTime(),
state: this.state
});
// Release distributed lock if acquired
await this.releaseLock();
}
/**
* Set lock releaser callback
* @internal Used by TransactionManager to integrate with distributed lock
*/ setLockReleaser(releaser) {
this.lockReleaser = releaser;
}
/**
* Release distributed lock
*/ async releaseLock() {
if (this.lockReleaser) {
try {
await this.lockReleaser();
logger.debug('Distributed lock released', {
transactionId: this.id
});
} catch (err) {
logger.error('Failed to release distributed lock', err, {
transactionId: this.id
});
}
}
}
/**
* Handle transaction timeout
*/ async handleTimeout() {
logger.warn('Transaction timeout exceeded', {
transactionId: this.id,
timeout: this.options.timeout,
duration: Date.now() - this.startedAt.getTime()
});
// Auto-rollback on timeout
await this.rollback();
}
/**
* Get transaction status
*/ getStatus() {
if (this.isCommitted) return 'committed';
if (this.isRolledBack) return 'rolled_back';
return 'active';
}
/**
* Get transaction duration in milliseconds
*/ getDuration() {
return Date.now() - this.startedAt.getTime();
}
}
/**
* Transaction Manager - manages transaction lifecycle and coordination
*/ export class TransactionManager {
activeTransactions = new Map();
adapters = new Map();
constructor(adapters){
if (adapters) {
this.adapters = adapters;
}
}
/**
* Register a database adapter
*/ registerAdapter(type, adapter) {
this.adapters.set(type, adapter);
logger.debug('Database adapter registered', {
type
});
}
/**
* Begin a new cross-database transaction
*/ async begin(databases, options) {
if (!databases || databases.length === 0) {
throw createDatabaseError(DatabaseErrorCode.VALIDATION_FAILED, 'At least one database must be specified', undefined, {
databases
});
}
// Validate all databases have adapters
for (const dbType of databases){
if (!this.adapters.has(dbType)) {
throw createDatabaseError(DatabaseErrorCode.VALIDATION_FAILED, `No adapter registered for database: ${dbType}`, undefined, {
database: dbType,
registered: Array.from(this.adapters.keys())
});
}
}
const txId = randomUUID();
const transaction = new Transaction(txId, databases, this.adapters, options);
// Begin the transaction
await transaction.begin();
this.activeTransactions.set(txId, transaction);
return transaction;
}
/**
* Get active transaction by ID
*/ getTransaction(id) {
return this.activeTransactions.get(id);
}
/**
* Get all active transactions
*/ getActiveTransactions() {
return Array.from(this.activeTransactions.values()).filter((tx)=>tx.getStatus() === 'active');
}
/**
* Get active transaction count
*/ getActiveCount() {
return this.getActiveTransactions().length;
}
/**
* Cleanup completed transactions from memory
*/ cleanupCompleted() {
let cleaned = 0;
for (const [id, tx] of Array.from(this.activeTransactions.entries())){
if (tx.getStatus() !== 'active') {
this.activeTransactions.delete(id);
cleaned++;
}
}
if (cleaned > 0) {
logger.debug('Cleaned up completed transactions', {
count: cleaned
});
}
return cleaned;
}
/**
* Force rollback of stale transactions (timeout cleanup)
*/ async cleanupStaleTransactions(maxAge = 60000) {
const now = Date.now();
const stale = [];
for (const tx of Array.from(this.activeTransactions.values())){
if (tx.getStatus() === 'active' && tx.getDuration() > maxAge) {
stale.push(tx);
}
}
for (const tx of stale){
try {
await tx.rollback();
logger.warn('Rolled back stale transaction', {
transactionId: tx.id,
age: tx.getDuration()
});
} catch (err) {
logger.error('Failed to rollback stale transaction', err, {
transactionId: tx.id
});
}
}
return stale.length;
}
/**
* Legacy: Execute operations across multiple databases atomically
* @deprecated Use begin() for new code
*/ async executeTransaction(adapters, operations) {
if (adapters.length !== operations.length) {
throw createDatabaseError(DatabaseErrorCode.VALIDATION_FAILED, `Adapters and operations arrays must be the same length`, undefined, {
adaptersLength: adapters.length,
operationsLength: operations.length
});
}
const databases = adapters.map((a)=>a.getType());
const tx = await this.begin(databases);
try {
const results = [];
for(let i = 0; i < operations.length; i++){
const result = await tx.execute(databases[i], operations[i]);
results.push(result);
}
await tx.commit();
return results;
} catch (err) {
await tx.rollback();
throw err;
}
}
}
//# sourceMappingURL=transaction-manager.js.map