BaasixBaasix

TasksService Documentation

← Back to Documentation Home

Table of Contents

  1. Overview
  2. Architecture
  3. Environment Configuration
  4. API Reference
  5. Usage in Extensions
  6. Best Practices
  7. 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 type
  • scheduled_time: DateTime field for when the task should be executed
  • attachment_id: Optional file attachment reference
  • task_data: JSON field for task-specific data
  • result_data: JSON field for storing task results
  • error_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=true

Optional 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=30

Example .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 shutdown

Important Limitations

  • Refresh Interval: Maximum allowed refresh interval is 3 hours (10800 seconds) to prevent missing tasks
  • Task Time Window: Only tasks with scheduled_time within 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 hours

Returns: 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 running

Parameters:

  • isRunning (boolean): true to mark a task as running, false when 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 checking

3. 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=true

Monitor Cache Stats

setInterval(async () => {
    const stats = await tasksService.getCacheStats();
    console.log("TasksService Stats:", stats);
}, 30000); // Every 30 seconds

Check 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
        });
    }
});

← Back to Documentation Home

On this page