fib-flow
Version:
A robust workflow management system for fibjs with task orchestration, state management, and distributed execution capabilities
954 lines (854 loc) • 39.2 kB
JavaScript
/**
* Provides a unified interface for task persistence across different database systems.
* Abstracts database-specific complexities to ensure consistent task management.
*/
const db = require('db');
const Pool = require('fib-pool');
const coroutine = require('coroutine');
const createLogger = require('../logger');
// Create logger for database operations
const logger = createLogger('fib-flow');
/**
* Centralizes task management logic to support distributed, resilient task processing.
* Enables flexible connection handling and provides a standardized task lifecycle.
*/
class BaseDBAdapter {
/**
* Configures connection pooling to optimize database resource utilization.
* Supports multiple connection initialization strategies for maximum flexibility.
*
* @param {string|object|function} conn - Flexible connection configuration
* @param {number} [poolSize=5] - Limits concurrent database connections
* @throws {Error} Prevents misconfigured database connections
*/
constructor(conn, poolSize = 5) {
logger.info(`[BaseDBAdapter] Initializing with connection type: ${typeof conn}, pool size: ${poolSize}`);
// Default lock clause for row-level locking
this.lockClause = 'FOR UPDATE SKIP LOCKED';
/**
* Normalizes different connection types to a consistent pool-like interface.
* Ensures uniform connection management regardless of underlying database.
*
* @param {*} conn - Raw database connection
* @returns {function} Standardized connection pool wrapper
*/
function wrap_conn(conn) {
logger.debug(`[BaseDBAdapter] Wrapping connection with thread-safe interface`);
// Thread-safe connection wrapper using coroutine lock
const locker = new coroutine.Lock();
// Provides a consistent execution and cleanup mechanism
const pool = function (callback) {
let result;
try {
// Ensure thread-safe execution of database operations
locker.acquire();
logger.debug(`[BaseDBAdapter] Acquired lock for database operation`);
result = callback(conn);
} catch (error) {
logger.error(`[BaseDBAdapter] Error during database operation:`, error);
throw error;
} finally {
// Always release the lock, even if an error occurs
logger.debug(`[BaseDBAdapter] Releasing lock after database operation`);
locker.release();
}
return result;
};
// Ensures thread-safe connection closure
pool.clear = function () {
try {
// Prevent concurrent connection closure
logger.debug(`[BaseDBAdapter] Acquiring lock for connection closure`);
locker.acquire();
conn.close();
logger.info(`[BaseDBAdapter] Connection closed successfully`);
} catch (error) {
logger.error(`[BaseDBAdapter] Error closing connection:`, error);
throw error;
} finally {
locker.release();
}
}.bind(this);
return pool;
}
// Prioritizes connection initialization based on connection type
// Prevents runtime errors and supports diverse database configurations
try {
if (conn == "sqlite::memory:") {
logger.info(`[BaseDBAdapter] Creating in-memory SQLite connection`);
this.pool = wrap_conn(db.open(conn));
}
else if (typeof conn === 'function') {
logger.debug(`[BaseDBAdapter] Using provided connection function`);
this.pool = conn;
}
else if (typeof conn === 'string') {
logger.info(`[BaseDBAdapter] Creating connection pool with connection string`);
this.pool = Pool({
create: () => {
logger.debug(`[BaseDBAdapter] Creating new connection in pool`);
return this.createConnection(conn);
},
destroy: conn => {
logger.debug(`[BaseDBAdapter] Destroying connection in pool`);
this.destroyConnection(conn);
},
timeout: 30000,
retry: 1,
maxsize: poolSize
});
}
else if (typeof conn === 'object') {
logger.debug(`[BaseDBAdapter] Wrapping provided connection object`);
this.pool = wrap_conn(conn);
}
else {
logger.error(`[BaseDBAdapter] Invalid connection type: ${typeof conn}`);
throw new Error('Invalid connection type: ' + conn);
}
logger.info(`[BaseDBAdapter] Connection pool initialized successfully`);
} catch (error) {
logger.error(`[BaseDBAdapter] Failed to initialize connection pool:`, error);
throw error;
}
}
/**
* Prepares database schema for task tracking.
* Enforces implementation requirement for specific database adapters.
* @throws {Error} If not overridden by subclass
*/
setup() {
logger.info(`[BaseDBAdapter] Setting up database schema`);
throw new Error('setup() must be implemented by subclass');
}
/**
* Establishes a database connection with error handling.
* Allows database-specific connection logic in subclasses.
*
* @param {string} connStr - Database connection parameters
* @returns {object} Initialized database connection
* @protected
*/
createConnection(connStr) {
logger.debug(`[BaseDBAdapter] Creating database connection with string: ${connStr}`);
return db.open(connStr);
}
/**
* Safely terminates database connection to prevent resource leaks.
* Provides a hook for custom connection cleanup in subclasses.
*
* @param {object} conn - Active database connection
* @protected
*/
destroyConnection(conn) {
logger.debug(`[BaseDBAdapter] Destroying database connection`);
conn.close();
}
/**
* Retrieves the ID of the last inserted row from database operation
* Provides a default implementation that works with common databases
* Each database adapter can override this method for specific implementations
*
* @protected
* @param {Object} conn - Database connection object
* @param {Object} rs - Result set from the insert operation
* @returns {number} The ID of the last inserted row
* @throws {Error} If the database doesn't support auto-incrementing IDs
*/
_getLastInsertedId(conn, rs) {
return rs.insertId;
}
/**
* Execute a callback within a transaction using execute method.
* Provides error handling and automatic rollback on failure.
*
* @protected
* @param {Object} conn - Database connection object
* @param {Function} callback - Function to execute within transaction
* @returns {*} Result of the callback execution
* @throws {Error} If transaction fails or callback throws an error
*/
_trans(conn, callback) {
logger.debug(`[BaseDBAdapter] Starting transaction`);
conn.execute('BEGIN');
try {
const callbackResult = callback();
conn.execute('COMMIT');
logger.debug(`[BaseDBAdapter] Transaction committed successfully`);
return callbackResult;
} catch (error) {
logger.error(`[BaseDBAdapter] Transaction failed:`, error);
conn.execute('ROLLBACK');
throw error;
}
}
/**
* Manages task insertion with comprehensive validation and workflow tracking.
* Supports atomic insertion of single or multiple tasks with parent-child relationships.
*
* @param {object|Array<object>} tasks - Tasks to be inserted
* @param {object} [options] - Insertion context and workflow metadata
* @returns {number|Array<number>} Assigned task ID(s)
* @throws {Error} If tasks fail validation or workflow constraints
*/
insertTask(tasks, options = {}) {
logger.info(`[BaseDBAdapter] Inserting tasks with options:`, options);
const isArray = Array.isArray(tasks);
const taskArray = isArray ? tasks : [tasks];
const taskIds = [];
this.pool(conn => this._trans(conn, () => {
const now = Math.floor(Date.now() / 1000);
// Updates parent task state to reflect child task creation
if (options.parent_id) {
logger.info(`[BaseDBAdapter] Updating parent task ${options.parent_id}`);
const rs = conn.execute(
`UPDATE fib_flow_tasks
SET total_children = total_children + ?,
status = 'suspended'
WHERE id = ? AND status = 'running'`,
taskArray.length,
options.parent_id
);
if (rs.affected === 0) {
logger.error(`[BaseDBAdapter] Parent task ${options.parent_id} is not in running state`);
throw new Error(`Parent task ${options.parent_id} is not in running state`);
}
}
for (const task of taskArray) {
// Enforces strict task definition requirements
if (!task) {
logger.error(`[BaseDBAdapter] Task object is required`);
throw new Error('Task object is required');
}
if (!task.name) {
logger.error(`[BaseDBAdapter] Task name is required`);
throw new Error('Task name is required');
}
if (!task.type) {
logger.error(`[BaseDBAdapter] Task type is required`);
throw new Error('Task type is required');
}
if (!['async', 'cron'].includes(task.type)) {
logger.error(`[BaseDBAdapter] Task type must be either "async" or "cron"`);
throw new Error('Task type must be either "async" or "cron"');
}
// Validate task status if provided
if (task.status && !['pending', 'running', 'completed', 'failed', 'timeout', 'permanently_failed', 'paused', 'suspended'].includes(task.status)) {
logger.error(`[BaseDBAdapter] Invalid task status: ${task.status}`);
throw new Error('Invalid task status');
}
logger.info(`[BaseDBAdapter] Inserting task with name: ${task.name}`);
const rs = conn.execute(
`INSERT INTO fib_flow_tasks (
name, type, status, priority, payload, cron_expr,
max_retries, retry_interval, next_run_time, timeout,
created_at, root_id, parent_id, total_children, completed_children,
tag
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?)`,
task.name,
task.type,
task.status || 'pending', // 使用提供的状态或默认为 pending
task.priority || 0,
task.payload ? JSON.stringify(task.payload) : null,
task.cron_expr || null,
task.max_retries !== undefined ? task.max_retries : 3,
task.retry_interval || 0,
task.next_run_time || now,
task.timeout || 60,
now,
options.root_id || null,
options.parent_id || null,
task.tag || null
);
taskIds.push(this._getLastInsertedId(conn, rs));
}
}));
return isArray ? taskIds : taskIds[0];
}
/**
* Implements a robust task claiming mechanism with concurrency control.
* Prevents race conditions and ensures fair task distribution among workers.
*
* @param {Array<string>} taskNames - Eligible task names for execution
* @param {string} workerId - ID of the worker claiming the task
* @returns {object|null} Next available task or null if no tasks are ready
* @throws {Error} If task name selection is invalid
*/
claimTask(taskNames, workerId) {
logger.info(`[claimTask] Attempting to claim task for worker ${workerId}, names:`, taskNames);
if (!Array.isArray(taskNames)) {
logger.error(`[claimTask] Invalid task names array:`, taskNames);
throw new Error('Task names array is required');
}
if (!workerId) {
logger.error(`[claimTask] Worker ID is required`);
throw new Error('Worker ID is required');
}
// Return early if taskNames is empty as no tasks can be claimed
if (taskNames.length === 0) {
logger.debug(`[claimTask] Empty task names array, skipping DB query`);
return null;
}
let task = null;
const now = Math.floor(Date.now() / 1000);
try {
this.pool(conn => {
while (true) {
// Find executable tasks
logger.debug(`[claimTask] Searching for pending tasks`);
const rs = conn.execute(
`SELECT * FROM fib_flow_tasks
WHERE status = 'pending'
AND name IN ?
AND next_run_time <= ?
ORDER BY priority DESC, next_run_time ASC
LIMIT 1
${this.lockClause}`,
taskNames,
now
);
if (rs.length > 0) {
// Update task status
logger.info(`[claimTask] Found pending task ${rs[0].id}, attempting to claim`);
const updateResult = conn.execute(
`UPDATE fib_flow_tasks
SET status = 'running',
last_active_time = ?,
worker_id = ?,
start_time = ?
WHERE id = ? AND status = 'pending'`,
now,
workerId,
now,
rs[0].id
);
if (updateResult.affected) {
task = rs[0];
logger.info(`[claimTask] Successfully claimed task ${task.id}`);
break;
}
logger.warning(`[claimTask] Task ${rs[0].id} was claimed by another worker`);
} else {
logger.debug(`[claimTask] No pending tasks found`);
break;
}
}
});
} catch (error) {
logger.error(`[claimTask] Error claiming task:`, error);
throw error;
}
if (task && task.payload) {
try {
logger.debug(`[claimTask] Parsing payload for task ${task.id}`);
task.payload = JSON.parse(task.payload);
} catch (error) {
logger.error(`[claimTask] Failed to parse payload for task ${task.id}:`, error);
}
}
return task;
}
/**
* Update task status with state transition validation
* Enforces valid state transitions to maintain task lifecycle integrity
* @param {string|number} taskId - ID of task to update
* @param {string} status - New status value (must be valid task state)
* @param {object} [extra] - Additional fields to update:
* - result: Task execution result
* - error: Error message if failed
* - next_run_time: Next scheduled run
* - retry_count: Current retry attempt
* @throws {Error} If status transition is invalid or update fails
*/
updateTaskStatus(taskId, status, extra = {}) {
logger.info(`[updateTaskStatus] Updating task ${taskId} to status '${status}' with extra:`, extra);
const allowedPreviousStatuses = {
'running': ['pending'],
'completed': ['running'],
'failed': ['running'],
'timeout': ['running'],
'pending': ['running', 'failed', 'timeout', 'paused', 'suspended'], // Add suspended
'permanently_failed': ['failed', 'timeout'],
'paused': ['running', 'failed', 'timeout'],
'suspended': ['running'] // Only running tasks can be suspended
};
if (!allowedPreviousStatuses[status]) {
logger.error(`[updateTaskStatus] Invalid status value: ${status}`);
throw new Error('Invalid status value');
}
try {
this.pool(conn => {
// Build dynamic SQL update statement
let updates = ['status = ?'];
let params = [status];
// Always update last_active_time for status changes
updates.push('last_active_time = ?');
params.push(Math.floor(Date.now() / 1000));
if ('result' in extra) {
updates.push('result = ?');
params.push(extra.result ? JSON.stringify(extra.result) : null);
}
if ('error' in extra) {
updates.push('error = ?');
params.push(extra.error);
}
if ('next_run_time' in extra) {
updates.push('next_run_time = ?');
params.push(extra.next_run_time);
}
if ('retry_count' in extra) {
updates.push('retry_count = ?');
params.push(extra.retry_count);
}
// Add WHERE clause parameters
params.push(taskId, allowedPreviousStatuses[status]);
logger.debug(`[updateTaskStatus] Executing update query with params:`, params);
this._trans(conn, () => {
const rs = conn.execute(`
UPDATE fib_flow_tasks
SET ${updates.join(', ')}
WHERE id = ? AND status IN ?
`, ...params);
if (rs.affected === 0) {
const rs1 = conn.execute(`SELECT status FROM fib_flow_tasks WHERE id = ?`, taskId);
logger.error(`[updateTaskStatus] Failed to update task ${taskId} - no rows affected. Current status: ${rs1[0].status}`);
throw new Error(`Failed to update task ${taskId}. Current status: ${rs1[0].status}`);
}
logger.info(`[updateTaskStatus] Successfully updated task ${taskId}, affected rows: ${rs.affected}`);
if (extra.parent_id) {
logger.info(`[updateTaskStatus] Updating parent task ${extra.parent_id}`);
const rs = conn.execute(`
UPDATE fib_flow_tasks
SET completed_children = completed_children + 1
WHERE id = ?;
UPDATE fib_flow_tasks
SET
status = 'pending',
stage = stage + 1
WHERE id = ? AND status = 'suspended' AND completed_children = total_children
`, extra.parent_id, extra.parent_id);
logger.info(`[updateTaskStatus] Parent task update completed for ${extra.parent_id}`);
}
});
});
} catch (error) {
logger.error(`[updateTaskStatus] Error updating task status:`, error);
throw error;
}
}
/**
* Update last active time for running tasks
* Used to track task health and detect timeouts
* @param {Array<string|number>} taskIds - IDs of tasks to update
*/
updateTaskActiveTime(taskIds) {
logger.info(`[updateTaskActiveTime] Updating active time for tasks:`, taskIds);
if (!Array.isArray(taskIds) || taskIds.length === 0) {
return;
}
const now = Math.floor(Date.now() / 1000);
return this.pool(conn => {
logger.debug(`[updateTaskActiveTime] Executing update query`);
return conn.execute(
'UPDATE fib_flow_tasks SET last_active_time = ? WHERE id IN ?',
now, taskIds
);
});
}
/**
* Handle tasks that have exceeded their timeout period
* Implements the following timeout handling logic:
* 1. Marks tasks as permanently_failed if max retries reached
* 2. Marks running tasks as timeout if inactive
* 3. Schedules retry attempts for failed tasks within retry limit
* @returns {object} Count of tasks in each state transition
*/
handleTimeoutTasks() {
logger.info(`[handleTimeoutTasks] Starting timeout task handling`);
const now = Math.floor(Date.now() / 1000);
try {
return this.pool(conn => {
// Set remaining timed-out tasks to 'timeout'
logger.debug(`[handleTimeoutTasks] Checking for timed-out tasks`);
const timeoutRs = conn.execute(
`UPDATE fib_flow_tasks
SET status = 'timeout',
last_active_time = ?
WHERE id IN (
SELECT id FROM (
SELECT id
FROM fib_flow_tasks
WHERE status = 'running'
AND start_time + timeout < ?
${this.lockClause}
) AS t
)`,
now, now
);
if (timeoutRs.affected) {
logger.warning(`[handleTimeoutTasks] ${timeoutRs.affected} tasks timed out`);
}
// Update tasks to 'pending' if retry interval has passed
logger.debug(`[handleTimeoutTasks] Checking for tasks eligible for retry`);
const retryRs = conn.execute(
`UPDATE fib_flow_tasks
SET status = 'pending',
retry_count = retry_count + 1,
last_active_time = ?,
next_run_time = ? + retry_interval
WHERE id IN (
SELECT id FROM (
SELECT id
FROM fib_flow_tasks
WHERE status IN ('timeout', 'failed')
AND retry_count < max_retries
AND (last_active_time + retry_interval < ?)
${this.lockClause}
) AS t
)`,
now, now, now
);
if (retryRs.affected) {
logger.info(`[handleTimeoutTasks] ${retryRs.affected} tasks scheduled for retry`);
}
// Update tasks with no retries left to 'permanently_failed' or 'paused' based on type
logger.debug(`[handleTimeoutTasks] Checking for tasks that have exhausted retries`);
const permanentlyFailedRs = conn.execute(
`UPDATE fib_flow_tasks
SET status = CASE type
WHEN 'cron' THEN 'paused'
ELSE 'permanently_failed'
END,
last_active_time = ?
WHERE id IN (
SELECT id FROM (
SELECT id
FROM fib_flow_tasks
WHERE status IN ('running', 'timeout', 'failed')
AND retry_count >= max_retries
AND (status != 'running' OR (last_active_time IS NOT NULL AND last_active_time + timeout < ?))
${this.lockClause}
) AS t
)`,
now, now
);
if (permanentlyFailedRs.affected) {
logger.warning(`[handleTimeoutTasks] ${permanentlyFailedRs.affected} tasks permanently failed`);
}
let workflowFailedCount = 0;
logger.debug(`[handleTimeoutTasks] Checking for workflow tasks to fail`);
while (true) {
const workflowFailedRs = conn.execute(
`UPDATE fib_flow_tasks
SET status = CASE type
WHEN 'cron' THEN 'paused'
ELSE 'permanently_failed'
END
WHERE id IN (
SELECT id FROM (
SELECT ft2.id
FROM fib_flow_tasks ft2
WHERE ft2.status = 'suspended'
AND EXISTS (
SELECT 1
FROM fib_flow_tasks child
WHERE child.status = 'permanently_failed'
AND child.parent_id = ft2.id
AND parent_id is not null
AND child.parent_id IN (
SELECT id
FROM fib_flow_tasks
WHERE status = 'suspended'
)
)
) t
${this.lockClause}
)`
);
workflowFailedCount += workflowFailedRs.affected;
if (workflowFailedRs.affected == 0) {
break;
}
logger.debug(`[handleTimeoutTasks] Found ${workflowFailedRs.affected} more workflow tasks to fail`);
}
if (workflowFailedCount) {
logger.notice(`[handleTimeoutTasks] ${workflowFailedCount} workflow tasks marked as failed`);
}
logger.debug(`[handleTimeoutTasks] Timeout task handling completed`);
});
} catch (error) {
logger.error(`[handleTimeoutTasks] Error handling timeout tasks:`, error);
throw error;
}
}
/**
* Retrieve task by ID with payload parsing
* @param {string|number} taskId - Task ID to retrieve
* @returns {object|null} Task object if found, null otherwise
*/
getTask(taskId) {
logger.info(`[getTask] Retrieving task with ID: ${taskId}`);
return this.pool(conn => {
const rs = conn.execute('SELECT * FROM fib_flow_tasks WHERE id = ?', taskId);
if (rs.length === 0) return null;
return this._parseTask(rs[0]);
});
}
/**
* Get all tasks with specified name
* @param {string} name - Task name to search for
* @returns {Array<object>} Array of matching tasks with parsed payloads
* @throws {Error} If name parameter is missing
*/
getTasksByName(name) {
logger.info(`[getTasksByName] Retrieving tasks with name: ${name}`);
if (!name) {
logger.error(`[getTasksByName] Task name is required`);
throw new Error('Task name is required');
}
return this.pool(conn => {
const tasks = conn.execute('SELECT * FROM fib_flow_tasks WHERE name = ?', name);
return tasks.map(task => this._parseTask(task));
});
}
/**
* Get all tasks with specified status
* @param {string} status - Status to filter by (must be valid task state)
* @returns {Array<object>} Array of matching tasks with parsed payloads
* @throws {Error} If status is invalid or missing
*/
getTasksByStatus(status) {
logger.info(`[getTasksByStatus] Retrieving tasks with status: ${status}`);
if (!status) {
logger.error(`[getTasksByStatus] Status is required`);
throw new Error('Status is required');
}
if (!['pending', 'running', 'completed', 'failed', 'timeout', 'permanently_failed', 'paused', 'suspended'].includes(status)) {
logger.error(`[getTasksByStatus] Invalid status value`);
throw new Error('Invalid status value');
}
return this.pool(conn => {
const tasks = conn.execute('SELECT * FROM fib_flow_tasks WHERE status = ?', status);
return tasks.map(task => this._parseTask(task));
});
}
/**
* Get tasks statistics by tag
* @param {string} tag - Tag to filter by (optional)
* @param {string} status - Status to filter by (optional)
* @returns {Array<object>} Array of task statistics grouped by tag and name
*/
getTaskStatsByTag(tag, status) {
logger.info(`[BaseDBAdapter] Getting task stats by tag: ${tag}, status: ${status}`);
return this.pool(conn => {
let sql = 'SELECT tag, name, status, COUNT(*) as count FROM fib_flow_tasks';
const params = [];
const conditions = [];
if (tag) {
conditions.push('tag = ?');
params.push(tag);
}
if (status) {
conditions.push('status = ?');
params.push(status);
}
if (conditions.length > 0) {
sql += ' WHERE ' + conditions.join(' AND ');
}
sql += ' GROUP BY tag, name, status ORDER BY tag, name, status';
logger.debug(`[BaseDBAdapter] Executing stats query: ${sql}`);
const result = conn.execute(sql, ...params);
logger.debug(`[BaseDBAdapter] Stats query returned ${result.length} rows`);
return result;
});
}
/**
* Get tasks by tag
* @param {string} tag - Tag to filter by
* @returns {Array<object>} Array of tasks with the specified tag
*/
getTasksByTag(tag) {
logger.info(`[BaseDBAdapter] Getting tasks by tag: ${tag}`);
if (!tag) {
logger.error(`[BaseDBAdapter] Tag is required`);
throw new Error('Tag is required');
}
return this.pool(conn => {
const tasks = conn.execute('SELECT * FROM fib_flow_tasks WHERE tag = ?', tag);
return tasks.map(task => this._parseTask(task));
});
}
/**
* Get tasks by multiple filter conditions
* @param {object} filters - Filter conditions
* @param {string} [filters.tag] - Filter by tag
* @param {string} [filters.status] - Filter by status (must be valid task state)
* @param {string} [filters.name] - Filter by task name
* @returns {Array<object>} Array of tasks matching all filter conditions
* @throws {Error} If status is invalid
*/
getTasks(filters = {}) {
logger.info(`[getTasks] Retrieving tasks with filters:`, filters);
// Validate status if provided
if (filters.status && !['pending', 'running', 'completed', 'failed', 'timeout', 'permanently_failed', 'paused', 'suspended'].includes(filters.status)) {
logger.error(`[getTasks] Invalid status value: ${filters.status}`);
throw new Error('Invalid status value');
}
return this.pool(conn => {
let sql = 'SELECT * FROM fib_flow_tasks';
const params = [];
const conditions = [];
if (filters.tag) {
conditions.push('tag = ?');
params.push(filters.tag);
}
if (filters.status) {
conditions.push('status = ?');
params.push(filters.status);
}
if (filters.name) {
conditions.push('name = ?');
params.push(filters.name);
}
if (conditions.length > 0) {
sql += ' WHERE ' + conditions.join(' AND ');
}
// Order by creation time descending to show newest first
sql += ' ORDER BY created_at DESC';
logger.debug(`[getTasks] Executing query: ${sql} with params:`, params);
const tasks = conn.execute(sql, ...params);
return tasks.map(task => this._parseTask(task));
});
}
/**
* Delete tasks by filter conditions
* @param {object} filters - Filter conditions
* @param {string} [filters.tag] - Filter by tag
* @param {string} [filters.status] - Filter by status (must be valid task state)
* @param {string} [filters.name] - Filter by task name
* @returns {number} Number of tasks deleted
* @throws {Error} If status is invalid
*/
deleteTasks(filters = {}) {
logger.info(`[deleteTasks] Deleting tasks with filters:`, filters);
// Validate status if provided
if (filters.status && !['pending', 'running', 'completed', 'failed', 'timeout', 'permanently_failed', 'paused', 'suspended'].includes(filters.status)) {
logger.error(`[deleteTasks] Invalid status value: ${filters.status}`);
throw new Error('Invalid status value');
}
return this.pool(conn => {
let sql = 'DELETE FROM fib_flow_tasks';
const params = [];
const conditions = [];
if (filters.tag) {
conditions.push('tag = ?');
params.push(filters.tag);
}
if (filters.status) {
conditions.push('status = ?');
params.push(filters.status);
}
if (filters.name) {
conditions.push('name = ?');
params.push(filters.name);
}
if (conditions.length > 0) {
sql += ' WHERE ' + conditions.join(' AND ');
}
logger.debug(`[deleteTasks] Executing query: ${sql} with params:`, params);
const result = conn.execute(sql, ...params);
logger.info(`[deleteTasks] Deleted ${result.affected} tasks`);
return result.affected;
});
}
/**
* Close database connection and cleanup resources
* Should be called when adapter is no longer needed
*/
close() {
logger.info(`[close] Closing database connection`);
if (this.pool) {
this.pool.clear();
this.pool = null;
}
}
/**
* Parses complex JSON fields to ensure data integrity and type safety.
* Gracefully handles parsing errors to prevent task processing interruption.
*
* @param {object} task - Unparsed task object from database
* @returns {object|null} Processed task with safely parsed JSON fields
* @private
*/
_parseTask(task) {
logger.debug(`[parseTask] Parsing task with ID: ${task.id}`);
if (!task) return null;
if (task.payload) {
try {
logger.debug(`[parseTask] Parsing payload for task ${task.id}`);
task.payload = JSON.parse(task.payload);
} catch (error) {
logger.error(`[parseTask] Failed to parse payload for task ${task.id}:`, error);
// Preserves original data if JSON parsing fails
task.payload = task.payload;
}
}
if (task.result) {
try {
logger.debug(`[parseTask] Parsing result for task ${task.id}`);
task.result = JSON.parse(task.result);
} catch (error) {
logger.error(`[parseTask] Failed to parse result for task ${task.id}:`, error);
// Preserves original data if JSON parsing fails
task.result = task.result;
}
}
return task;
}
/**
* Provides real-time visibility into active task processing.
* Enables monitoring and potential intervention for long-running tasks.
*
* @returns {Array<object>} Currently executing tasks with parsed metadata
*/
getRunningTasks() {
logger.info(`[getRunningTasks] Retrieving running tasks`);
return this.pool(conn => {
const tasks = conn.execute('SELECT * FROM fib_flow_tasks WHERE status = ?', 'running');
return tasks.map(task => this._parseTask(task));
});
}
/**
* Allows complete task database reset for testing or maintenance.
* Provides a controlled mechanism to clear all task records.
*
* @returns {number} Count of tasks permanently removed
*/
clearTasks() {
logger.info(`[clearTasks] Clearing all tasks`);
return this.pool(conn => {
const rs = conn.execute('DELETE FROM fib_flow_tasks');
return rs.affected;
});
}
/**
* Retrieves task hierarchy to support complex workflow tracking.
* Enables understanding of task dependencies and execution context.
*
* @param {string|number} parentId - Identifier for parent task
* @returns {Array<object>} Detailed child task information
* @throws {Error} Prevents queries without a valid parent task ID
*/
getChildTasks(parentId) {
logger.info(`[getChildTasks] Retrieving child tasks for parent ID: ${parentId}`);
if (!parentId) {
logger.error(`[getChildTasks] Parent task ID is required`);
throw new Error('Parent task ID is required');
}
return this.pool(conn => {
const tasks = conn.execute(`SELECT * FROM fib_flow_tasks WHERE parent_id = ?`, parentId);
return tasks.map(task => this._parseTask(task));
});
}
}
module.exports = BaseDBAdapter;