dynamiccronjobs
- community
- community-management
- by Limon
- Takaro main
- all
Dynamic CronJob Manager: Automated Game Server Command Scheduler
π₯ SETUP TUTORIAL
βΊ Watch the Complete Guide
Learn how to set up and configure dynamic cronjobs
Create and manage dynamic cronjobs for executing game server commands based on flexible schedules. Define commands and their cron expressions to automate tasks.
- Dynamic Updates: The module's
cronJobGeneratorcronjob automatically checks for and adapts to changes in the user configuration. This ensures that any modifications to commands or schedules are applied dynamically. - Cron Syntax Validation: The module validates the syntax of cron expressions. If an invalid expression is provided, that specific cronjob will be skipped, preventing errors and ensuring only valid schedules are applied. For assistance with cron syntax, you can use a tool like crontab.guru.
- Command Sequencing: You can define multiple commands within a single cronjob entry, separated by semicolons. These commands will be executed sequentially according to the defined schedule.
- Game Server Specificity: It is crucial to install the latest version of this module and create a separate copy for each game server. This ensures that cronjobs are executed on the intended server and avoids unintended cross-server execution.
- Automated Management: The module handles the creation, updating, and cleanup of cronjobs. It removes any cronjobs that are no longer present in the configuration, maintaining a clean and accurate schedule.
- Internal Synchronization: To ensure proper synchronization of cron schedules, the module may perform internal operations to update its configuration. This process is automated and does not require manual intervention from the user.
Configuration 1
Settings you fill in when installing the module on a server.
| Setting | Type | Default | Description |
|---|---|---|---|
cronjobs Cronjobs | array | β | List of cronjobs and their schedules |
Raw config schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"cronjobs": {
"type": "array",
"title": "Cronjobs",
"description": "List of cronjobs and their schedules",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Optional name for the cronjob",
"minLength": 1
},
"command": {
"type": "string",
"description": "You can enter multiple commands separated by semicolons (;) to create a sequence of scheduled tasks for the same cron expression. ",
"minLength": 1
},
"temporalValue": {
"type": "string",
"description": "Cron expression for execution schedule",
"minLength": 1
}
},
"required": [
"command",
"temporalValue"
]
}
}
},
"additionalProperties": false
} Raw UI schema
{} Cron jobs 1
Work the module runs on a schedule.
-
cronJobGenerator
Check changes in the user config and adapts (optimized: runs every 15 minutes with smart change detection)
Cron job source
import { takaro, data } from '@takaro/helpers'; async function main() { const { gameServerId, module: mod } = data; const cronjobs = mod.userConfig.cronjobs || []; console.log('π Dynamic Cronjob Generator starting...'); // State management using variables const configHashKey = 'config_hash'; const lastProcessedKey = 'last_processed'; const backupConfigKey = 'backup_config'; try { // Create configuration hash for change detection const currentConfigHash = JSON.stringify(cronjobs).split('').reduce((a, b) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0).toString(); // Get stored state let storedHash = null; let lastProcessed = null; try { const hashVar = await takaro.variable.variableControllerSearch({ filters: { key: [configHashKey], moduleId: [mod.moduleId], gameServerId: [gameServerId] } }); if (hashVar.data.data.length > 0) { storedHash = hashVar.data.data[0].value; } const processedVar = await takaro.variable.variableControllerSearch({ filters: { key: [lastProcessedKey], moduleId: [mod.moduleId], gameServerId: [gameServerId] } }); if (processedVar.data.data.length > 0) { lastProcessed = new Date(processedVar.data.data[0].value); } } catch (error) { console.log('π No previous state found, treating as first run'); } // Check if configuration changed if (storedHash === currentConfigHash && lastProcessed && (Date.now() - lastProcessed.getTime()) < 300000) { // 5 minutes console.log('β No configuration changes detected, skipping update'); return; } // Backup current configuration try { await takaro.variable.variableControllerCreate({ key: backupConfigKey, value: JSON.stringify({ userConfig: mod.userConfig, timestamp: new Date().toISOString(), hash: currentConfigHash }), gameServerId, moduleId: mod.moduleId }); console.log('πΎ Configuration backup created'); } catch (backupError) { // Update existing backup const existingBackup = await takaro.variable.variableControllerSearch({ filters: { key: [backupConfigKey], moduleId: [mod.moduleId], gameServerId: [gameServerId] } }); if (existingBackup.data.data.length > 0) { await takaro.variable.variableControllerUpdate(existingBackup.data.data[0].id, { value: JSON.stringify({ userConfig: mod.userConfig, timestamp: new Date().toISOString(), hash: currentConfigHash }) }); console.log('πΎ Configuration backup updated'); } } // Validate cronjobs first const validatedCronjobs = []; const invalidJobs = []; function isValidCronSyntax(cronExpression) { try { cronExpression = cronExpression.trim(); // Check for special expressions if (/^@(yearly|annually|monthly|weekly|daily|hourly|reboot)$/.test(cronExpression)) { return true; } const parts = cronExpression.split(/\s+/); if (parts.length !== 5) { return false; } // Valid characters per field const validPatterns = [ /^[0-9,\-*\/]+$/, // Minutes /^[0-9,\-*\/]+$/, // Hours /^[0-9,\-*\/?]+$/, // Day of month /^[0-9,\-*\/]+$/, // Month /^[0-9,\-*\/?a-zA-Z]+$/ // Day of week ]; for (let i = 0; i < 5; i++) { if (!validPatterns[i].test(parts[i])) { return false; } } // Range validation const minutes = parts[0].split(/[,\-\/]/).filter(m => m !== '*' && /^\d+$/.test(m)); if (minutes.some(m => parseInt(m) > 59)) return false; const hours = parts[1].split(/[,\-\/]/).filter(h => h !== '*' && /^\d+$/.test(h)); if (hours.some(h => parseInt(h) > 23)) return false; const dom = parts[2].split(/[,\-\/]/).filter(d => d !== '*' && d !== '?' && /^\d+$/.test(d)); if (dom.some(d => parseInt(d) < 1 || parseInt(d) > 31)) return false; const months = parts[3].split(/[,\-\/]/).filter(m => m !== '*' && /^\d+$/.test(m)); if (months.some(m => parseInt(m) < 1 || parseInt(m) > 12)) return false; return true; } catch (err) { return false; } } // Validate all cronjobs for (let i = 0; i < cronjobs.length; i++) { const job = cronjobs[i]; if (!isValidCronSyntax(job.temporalValue)) { console.log(`β Invalid cron syntax in job ${job.name || i + 1}: "${job.temporalValue}"`); invalidJobs.push(job); continue; } validatedCronjobs.push(job); } if (invalidJobs.length > 0) { console.log(`β οΈ Skipped ${invalidJobs.length} invalid cronjob(s)`); } // Get existing cronjobs for this module const existingCronjobs = (await takaro.cronjob.cronJobControllerSearch({ filters: { versionId: [mod.versionId] } })).data.data; const processedJobs = new Set(); let jobCounter = 1; let successCount = 0; let errorCount = 0; // Process validated cronjobs with individual CRUD operations for (let i = 0; i < validatedCronjobs.length; i++) { const job = validatedCronjobs[i]; const commands = job.command.split(';').map(cmd => cmd.trim()).filter(cmd => cmd); for (let cmdIndex = 0; cmdIndex < commands.length; cmdIndex++) { const command = commands[cmdIndex]; const seqSuffix = commands.length > 1 ? `-seq${cmdIndex + 1}` : ''; const jobNameSuffix = job.name ? `-${job.name}${seqSuffix}` : seqSuffix; const jobName = `cr-${jobCounter}${jobNameSuffix}`; jobCounter++; processedJobs.add(jobName); const jobFunction = ` import { takaro, data } from '@takaro/helpers'; async function main() { const { gameServerId } = data; await takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { command: \`${command}\`, }); } await main(); `.trim(); try { const existingJob = existingCronjobs.find(j => j.name === jobName); if (existingJob) { // Only update if temporal value changed if (existingJob.temporalValue !== job.temporalValue) { console.log(`π Updating cronjob ${jobName}: ${existingJob.temporalValue} β ${job.temporalValue}`); await takaro.cronjob.cronJobControllerUpdate(existingJob.id, { name: jobName, temporalValue: job.temporalValue, function: jobFunction }); successCount++; } } else { console.log(`β Creating new cronjob: ${jobName}`); try { await takaro.cronjob.cronJobControllerCreate({ name: jobName, temporalValue: job.temporalValue, versionId: mod.versionId, function: jobFunction }); successCount++; } catch (createError) { if (createError.response?.status === 409) { // Handle conflict by searching and updating const conflictJobSearch = await takaro.cronjob.cronJobControllerSearch({ filters: { name: [jobName] } }); if (conflictJobSearch.data.data.length > 0) { const conflictJob = conflictJobSearch.data.data[0]; await takaro.cronjob.cronJobControllerUpdate(conflictJob.id, { name: jobName, temporalValue: job.temporalValue, function: jobFunction }); successCount++; console.log(`π Resolved conflict and updated: ${jobName}`); } else { throw createError; } } else { throw createError; } } } } catch (error) { console.log(`β Error with cronjob "${jobName}": ${error.message || "Unknown error"}`); errorCount++; } } } // Clean up orphaned cronjobs let cleanupCount = 0; for (const existingJob of existingCronjobs) { if (/^cr-\d+(-.*)?$/.test(existingJob.name) && !processedJobs.has(existingJob.name)) { try { await takaro.cronjob.cronJobControllerRemove(existingJob.id); cleanupCount++; console.log(`ποΈ Removed orphaned cronjob: ${existingJob.name}`); } catch (error) { console.log(`β Error removing cronjob "${existingJob.name}": ${error.message}`); errorCount++; } } } // Update state variables try { // Update config hash const hashVarSearch = await takaro.variable.variableControllerSearch({ filters: { key: [configHashKey], moduleId: [mod.moduleId], gameServerId: [gameServerId] } }); if (hashVarSearch.data.data.length > 0) { await takaro.variable.variableControllerUpdate(hashVarSearch.data.data[0].id, { value: currentConfigHash }); } else { await takaro.variable.variableControllerCreate({ key: configHashKey, value: currentConfigHash, gameServerId, moduleId: mod.moduleId }); } // Update last processed timestamp const processedVarSearch = await takaro.variable.variableControllerSearch({ filters: { key: [lastProcessedKey], moduleId: [mod.moduleId], gameServerId: [gameServerId] } }); if (processedVarSearch.data.data.length > 0) { await takaro.variable.variableControllerUpdate(processedVarSearch.data.data[0].id, { value: new Date().toISOString() }); } else { await takaro.variable.variableControllerCreate({ key: lastProcessedKey, value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }); } console.log('π State variables updated successfully'); } catch (stateError) { console.log(`β οΈ Warning: Could not update state variables: ${stateError.message}`); } console.log(`β Dynamic Cronjob Generator completed:`); console.log(` π ${successCount} operations successful`); console.log(` ποΈ ${cleanupCount} orphaned jobs cleaned`); if (errorCount > 0) { console.log(` β ${errorCount} errors encountered`); } } catch (error) { console.log(`π₯ Critical error in Dynamic Cronjob Generator: ${error.message}`); // Attempt to restore from backup if available try { const backupSearch = await takaro.variable.variableControllerSearch({ filters: { key: [backupConfigKey], moduleId: [mod.moduleId], gameServerId: [gameServerId] } }); if (backupSearch.data.data.length > 0) { const backup = JSON.parse(backupSearch.data.data[0].value); console.log(`π Backup available from ${backup.timestamp}`); console.log(' Manual restoration may be required if issues persist'); } } catch (backupError) { console.log('β οΈ No backup available for restoration'); } throw error; // Re-throw to ensure error is logged in events } } await main();