TasksService Documentation
Table of Contents
- Overview
- Architecture
- Environment Configuration
- API Reference
- Usage in Extensions
- Best Practices
- Troubleshooting
Overview
The TasksService is a built-in service that manages background tasks efficiently by caching "Not started" tasks from the baasix_Tasks table and coordinating task execution across the system. It reduces database calls and ensures only one task runs at a time.
Key Features
- Intelligent Caching: Keeps "Not started" tasks in Redis cache to minimize database queries
- Time-Filtered Caching: Only caches tasks scheduled within 4 hours to reduce memory usage
- Automatic Refresh: Periodically refreshes cache based on configurable intervals (max 3 hours)
- Change Detection: Automatically invalidates cache when tasks are created, updated, or deleted
- Task Coordination: Provides global state management to prevent concurrent task execution
- Graceful Shutdown: Waits for running tasks to complete during server shutdown
- Extension Integration: Easily integrates with custom extensions for task processing
Architecture
baasix_Tasks Table Structure
The TasksService works with the built-in baasix_Tasks table, which includes:
id: Primary key (auto-increment)task_status: ENUM with values:"Not started"- Tasks available for processing"Running"- Currently executing tasks"Completed"- Successfully finished tasks"Error"- Failed tasks
type: String field defining the task typescheduled_time: DateTime field for when the task should be executedattachment_id: Optional file attachment referencetask_data: JSON field for task-specific dataresult_data: JSON field for storing task resultserror_data: JSON field for storing error information- Standard audit fields (userCreated, userUpdated, createdAt, updatedAt)
Service Architecture
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Extensions │───▶│ TasksService │───▶│ Redis Cache │
│ │ │ │ │ │
│ - Schedule Exts │ │ - getNotStarted │ │ - Task Cache │
│ - Hook Exts │ │ - setRunning │ │ - Running State │
│ - Custom Logic │ │ - isRunning │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ baasix_Tasks │
│ PostgreSQL │
└─────────────────┘Environment Configuration
Configure TasksService behavior with these environment variables:
Required Variables
# Redis cache connection (shared with other caching)
CACHE_REDIS_URL=redis://localhost:6379
CACHE_ENABLED=trueOptional Variables
# Task cache refresh interval in seconds (default: 600, maximum: 10800 = 3 hours)
TASK_LIST_REFRESH_INTERVAL=600
# Maximum time to wait for running tasks during shutdown in seconds (default: 30)
TASK_SHUTDOWN_WAIT_TIME=30Example .env Configuration
CACHE_REDIS_URL=redis://localhost:6379
CACHE_ENABLED=true
TASK_LIST_REFRESH_INTERVAL=1800 # Refresh every 30 minutes (max 3 hours enforced)
TASK_SHUTDOWN_WAIT_TIME=60 # Wait up to 1 minute for tasks during shutdownImportant Limitations
- Refresh Interval: Maximum allowed refresh interval is 3 hours (10800 seconds) to prevent missing tasks
- Task Time Window: Only tasks with
scheduled_timewithin 4 hours from current time are cached - Memory Optimization: Time filtering reduces memory usage by excluding far-future tasks
API Reference
Core Methods
getNotStartedTasks()
Retrieves cached "Not started" tasks from the baasix_Tasks table that are scheduled within 4 hours.
const tasks = await tasksService.getNotStartedTasks();
// Returns: Array of task objects with task_status: "Not started" and scheduled_time within 4 hoursReturns: Promise<Array> - Array of task objects (filtered by scheduled_time)
Throws: Never throws, returns empty array on error
Note: Only returns tasks scheduled within 4 hours from current time
setTaskRunning(isRunning)
Sets the global task running state to coordinate task execution.
await tasksService.setTaskRunning(true); // Mark task as running
await tasksService.setTaskRunning(false); // Mark task as not runningParameters:
isRunning(boolean):trueto mark a task as running,falsewhen done
Returns: Promise<void>
isTaskRunning()
Checks if any task is currently running in the system.
const isRunning = await tasksService.isTaskRunning();
if (isRunning) {
console.log("A task is already running");
return; // Skip task execution
}Returns: Promise<boolean> - true if a task is running, false otherwise
forceRefresh()
Manually triggers a cache refresh, useful for testing or immediate updates.
await tasksService.forceRefresh();
console.log("Cache refreshed manually");Returns: Promise<void>
getCacheStats()
Returns statistics about the cache and service status.
const stats = await tasksService.getCacheStats();
console.log(stats);
// {
// cachedTasksCount: 5,
// isTaskRunning: false,
// refreshInterval: 600000,
// refreshIntervalSeconds: 600,
// maxRefreshIntervalSeconds: 10800,
// taskTimeWindow: "4 hours",
// initialized: true,
// lastRefreshed: "2023-12-01T10:30:00.000Z"
// }Returns: Promise<Object> - Cache statistics object
Usage in Extensions
Schedule Extension Example
Create a schedule extension that processes tasks periodically:
// extensions/baasix-schedule-task-processor/index.js
import schedule from "node-schedule";
import tasksService from "../../baasix/services/TasksService";
import { getSequelize } from "../../baasix/utils/database";
// Run every 5 minutes
const job = schedule.scheduleJob("*/5 * * * *", async function () {
console.log("Task processor started");
// Check if another task is already running
if (await tasksService.isTaskRunning()) {
console.log("Task already running, skipping...");
return;
}
// Mark task as running
await tasksService.setTaskRunning(true);
try {
// Get available tasks
const tasks = await tasksService.getNotStartedTasks();
if (tasks.length === 0) {
console.log("No tasks to process");
return;
}
console.log(`Processing ${tasks.length} tasks`);
// Process first available task
const task = tasks[0];
await processTask(task);
} catch (error) {
console.error("Task processing error:", error);
} finally {
// Always mark task as not running
await tasksService.setTaskRunning(false);
}
});
async function processTask(task) {
const sequelize = getSequelize();
const TasksModel = sequelize.models.baasix_Tasks;
try {
// Update task status to Running
await TasksModel.update(
{ task_status: "Running" },
{ where: { id: task.id } }
);
// Simulate task processing based on task.type
switch (task.type) {
case "email_batch":
await processBatchEmails(task);
break;
case "data_export":
await processDataExport(task);
break;
case "cleanup":
await processCleanup(task);
break;
default:
throw new Error(`Unknown task type: ${task.type}`);
}
// Mark task as completed
await TasksModel.update(
{ task_status: "Completed" },
{ where: { id: task.id } }
);
console.log(`Task ${task.id} completed successfully`);
} catch (error) {
// Mark task as error
await TasksModel.update(
{
task_status: "Error",
error_message: error.message
},
{ where: { id: task.id } }
);
console.error(`Task ${task.id} failed:`, error);
throw error;
}
}
export default job;Hook Extension Example
Create a hook that creates tasks based on certain events:
// extensions/baasix-hook-task-creator/index.js
import hooksService from "../../baasix/services/HooksService";
import tasksService from "../../baasix/services/TasksService";
import { getSequelize } from "../../baasix/utils/database";
// Create a task when a large data import is completed
hooksService.registerHook("data_imports", "items.create.after", async ({ data }) => {
if (data.status === "completed" && data.record_count > 1000) {
const sequelize = getSequelize();
const TasksModel = sequelize.models.baasix_Tasks;
// Create a cleanup task
await TasksModel.create({
type: "cleanup_temp_files",
task_status: "Not started",
metadata: JSON.stringify({
import_id: data.id,
temp_files: data.temp_files
})
});
console.log(`Created cleanup task for import ${data.id}`);
// Optionally force refresh cache for immediate availability
await tasksService.forceRefresh();
}
return data;
});Best Practices
1. Always Use try/finally for Task State
// ✅ Good - Always reset running state
await tasksService.setTaskRunning(true);
try {
// Process tasks
} finally {
await tasksService.setTaskRunning(false);
}
// ❌ Bad - State might not be reset on error
await tasksService.setTaskRunning(true);
// Process tasks
await tasksService.setTaskRunning(false);2. Check Running State Before Starting Tasks
// ✅ Good - Check before starting
if (await tasksService.isTaskRunning()) {
return; // Skip if already running
}
// ❌ Bad - Multiple tasks might run concurrently
// Start processing without checking3. Handle Task Status Updates Properly
// ✅ Good - Update task status in database
await TasksModel.update(
{ task_status: "Running" },
{ where: { id: task.id } }
);
// Process task...
await TasksModel.update(
{ task_status: "Completed" },
{ where: { id: task.id } }
);4. Use Appropriate Refresh Intervals
# ✅ Good - For high-frequency task creation
TASK_LIST_REFRESH_INTERVAL=60 # 1 minute
# ✅ Good - For normal usage
TASK_LIST_REFRESH_INTERVAL=1800 # 30 minutes
# ⚠️ Maximum enforced - Longest allowed interval
TASK_LIST_REFRESH_INTERVAL=10800 # 3 hours (system maximum)
# ❌ Bad - Will be capped at 3 hours
TASK_LIST_REFRESH_INTERVAL=14400 # 4 hours (automatically reduced to 3 hours)5. Implement Proper Error Handling
try {
await processTask(task);
await TasksModel.update(
{ task_status: "Completed" },
{ where: { id: task.id } }
);
} catch (error) {
await TasksModel.update(
{
task_status: "Error",
error_message: error.message
},
{ where: { id: task.id } }
);
throw error; // Re-throw to trigger finally block
}Troubleshooting
Common Issues
1. Tasks Not Appearing in Cache
Symptoms: getNotStartedTasks() returns empty array despite having "Not started" tasks in database.
Solutions:
// Check service initialization
const stats = await tasksService.getCacheStats();
console.log("Service initialized:", stats.initialized);
console.log("Task time window:", stats.taskTimeWindow);
// Force refresh cache
await tasksService.forceRefresh();
// Check database directly (including time filter)
const TasksModel = sequelize.models.baasix_Tasks;
const fourHoursFromNow = new Date();
fourHoursFromNow.setHours(fourHoursFromNow.getHours() + 4);
const dbTasks = await TasksModel.findAll({
where: {
task_status: "Not started",
scheduled_time: {
[sequelize.Sequelize.Op.lte]: fourHoursFromNow
}
}
});
console.log("Tasks in DB (within 4 hours):", dbTasks.length);2. Tasks Stuck in "Running" State
Symptoms: Tasks remain in "Running" status and isTaskRunning() always returns true.
Solutions:
// Reset running state manually
await tasksService.setTaskRunning(false);
// Check for tasks stuck in running state
const TasksModel = sequelize.models.baasix_Tasks;
const runningTasks = await TasksModel.findAll({
where: { task_status: "Running" }
});
// Reset stuck tasks if needed
await TasksModel.update(
{ task_status: "Error", error_message: "Manually reset" },
{ where: { task_status: "Running" } }
);3. Cache Not Refreshing
Symptoms: Cache doesn't update when tasks are created/updated/deleted.
Solutions:
// Check hook registration
console.log("TasksService hooks registered");
// Verify cache connection
const cache = getCache();
try {
await cache.set("test", "value");
await cache.get("test");
console.log("Cache connection working");
} catch (error) {
console.error("Cache connection failed:", error);
}Debugging Tips
Enable Debug Logging
DEBUGGING=trueMonitor Cache Stats
setInterval(async () => {
const stats = await tasksService.getCacheStats();
console.log("TasksService Stats:", stats);
}, 30000); // Every 30 secondsCheck Service Health
// Add to your monitoring endpoint
app.get('/health/tasks', async (req, res) => {
try {
const stats = await tasksService.getCacheStats();
const isHealthy = stats.initialized && !stats.error;
res.status(isHealthy ? 200 : 500).json({
healthy: isHealthy,
stats
});
} catch (error) {
res.status(500).json({
healthy: false,
error: error.message
});
}
});Related Documentation
- Baasix Extensions - Creating custom extensions
- Hooks System - Understanding the hooks system
- Additional Features - Cache management
- Database Schema - Understanding the data model