From f50b1da25477deb2888d977491084e2fdffe900b Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Mon, 12 Jan 2026 19:18:18 -0400 Subject: [PATCH 1/2] Add atomic task claiming and stale task release functionality - Introduced atomic claiming mechanism in TaskSchedulerManager to prevent duplicate task executions. - Added CLAIM_TIMEOUT_MINUTES constant to define the timeout for stale claimed tasks. - Implemented releaseStaleClaimedTasks method to release tasks that have been claimed for too long. - Refactored scheduleTasks method to utilize the new atomic claim logic. - Updated ScheduledTask model to include claimed_by and claimed_at fields. - Created migration to add claimed fields to the scheduled_tasks table. - Added tests to ensure atomic claim functionality and stale claim release behavior. --- .../Managers/TaskSchedulerManager.php | 246 ++++++++--- ProcessMaker/Models/ScheduledTask.php | 5 + ..._claim_fields_to_scheduled_tasks_table.php | 37 ++ .../Feature/ScheduledTaskDuplicationTest.php | 418 ++++++++++++++++++ 4 files changed, 638 insertions(+), 68 deletions(-) create mode 100644 database/migrations/2026_01_12_000000_add_claim_fields_to_scheduled_tasks_table.php create mode 100644 tests/Feature/ScheduledTaskDuplicationTest.php diff --git a/ProcessMaker/Managers/TaskSchedulerManager.php b/ProcessMaker/Managers/TaskSchedulerManager.php index f992010de2..cb54a8802e 100644 --- a/ProcessMaker/Managers/TaskSchedulerManager.php +++ b/ProcessMaker/Managers/TaskSchedulerManager.php @@ -12,6 +12,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Schema; +use Illuminate\Support\Str; use PDOException; use ProcessMaker\Facades\WorkflowManager; use ProcessMaker\Jobs\StartEventConditional; @@ -133,89 +134,198 @@ private function scheduleTask( } /** - * Checks the schedule_tasks table to execute jobs + * Timeout in minutes for stale claimed tasks. + * If a task has been claimed for longer than this, it will be released. + */ + const CLAIM_TIMEOUT_MINUTES = 5; + + /** + * Checks the schedule_tasks table to execute jobs. + * Uses atomic claim per task to prevent duplicate executions while maintaining + * the original selection logic (nextDate calculation). */ public function scheduleTasks() { $today = $this->today(); + $todayFormatted = $today->format('Y-m-d H:i:s'); + try { - /** - * This validation is removed; the database schema should exist before - * any initiation of 'jobs' and 'schedule'. - * - * if (!Schema::hasTable('scheduled_tasks')) { - * return; - * } - */ $this->removeExpiredLocks(); - $tasks = ScheduledTask::cursor(); + // 1. Release stale claims (tasks that were claimed but never completed) + $this->releaseStaleClaimedTasks(); + + // 2. Get candidate tasks using cursor() for memory efficiency + // We filter by unclaimed tasks only, but evaluate nextDate for each + $tasks = ScheduledTask::whereNull('claimed_by')->cursor(); foreach ($tasks as $task) { - try { - $config = json_decode($task->configuration); - - $lastExecution = new DateTime($task->last_execution, new DateTimeZone('UTC')); - - if ($lastExecution === null) { - continue; - } - $owner = $task->processRequestToken ?: $task->processRequest ?: $task->process; - $ownerDateTime = $owner?->created_at; - $nextDate = $this->nextDate($today, $config, $lastExecution, $ownerDateTime); - - // if no execution date exists we go to the next task - if (empty($nextDate)) { - continue; - } - - // Since the task scheduler has a presition of 1 minute (crontab) - // the times must be rounded or trucated to the nearest HH:MM:00 before compare - $method = config('app.timer_events_seconds') . 'DateTime'; - $todayWithoutSeconds = $this->$method($today); - $nextDateWithoutSeconds = $this->$method($nextDate); - if ($nextDateWithoutSeconds <= $todayWithoutSeconds) { - switch ($task->type) { - case 'TIMER_START_EVENT': - $this->executeTimerStartEvent($task, $config); - $task->last_execution = $today->format('Y-m-d H:i:s'); - $task->save(); - break; - case 'INTERMEDIATE_TIMER_EVENT': - $executed = $this->executeIntermediateTimerEvent($task, $config); - $task->last_execution = $today->format('Y-m-d H:i:s'); - if ($executed) { - $task->save(); - } - break; - case 'BOUNDARY_TIMER_EVENT': - $executed = $this->executeBoundaryTimerEvent($task, $config); - $task->last_execution = $today->format('Y-m-d H:i:s'); - if ($executed) { - $task->save(); - } - break; - case 'SCHEDULED_JOB': - $this->executeScheduledJob($config); - $task->last_execution = $today->format('Y-m-d H:i:s'); - $task->save(); - break; - default: - throw new Exception('Unknown timer event: ' . $task->type); - } - } - } catch (\Throwable $ex) { - Log::Error('Failed Scheduled Task: ', [ - 'Task data' => print_r($task->getAttributes(), true), - 'Exception' => $ex->__toString(), - ]); - } + $this->processTaskWithAtomicClaim($task, $today, $todayFormatted); } } catch (PDOException $e) { Log::error('The connection to the database had problems (scheduleTasks): ' . $e->getMessage()); } } + /** + * Release tasks that have been claimed for too long (stale claims). + * This handles cases where a process crashed after claiming tasks. + */ + private function releaseStaleClaimedTasks(): void + { + $staleThreshold = Carbon::now()->subMinutes(self::CLAIM_TIMEOUT_MINUTES); + + ScheduledTask::whereNotNull('claimed_by') + ->where('claimed_at', '<', $staleThreshold) + ->update([ + 'claimed_by' => null, + 'claimed_at' => null, + ]); + } + + /** + * Process a task with atomic claim to prevent duplicate execution. + * This maintains the original selection logic (nextDate calculation) while + * adding protection against concurrent execution. + * + * @param ScheduledTask $task The task to evaluate and potentially execute + * @param DateTime $today Current datetime + * @param string $todayFormatted Formatted datetime string + */ + private function processTaskWithAtomicClaim(ScheduledTask $task, DateTime $today, string $todayFormatted): void + { + try { + $config = json_decode($task->configuration); + $lastExecution = new DateTime($task->last_execution, new DateTimeZone('UTC')); + + if ($lastExecution === null) { + return; + } + + $owner = $task->processRequestToken ?: $task->processRequest ?: $task->process; + $ownerDateTime = $owner?->created_at; + $nextDate = $this->nextDate($today, $config, $lastExecution, $ownerDateTime); + + // If no execution date exists, skip this task + if (empty($nextDate)) { + return; + } + + // Since the task scheduler has a precision of 1 minute (crontab) + // the times must be rounded or truncated to the nearest HH:MM:00 before compare + $method = config('app.timer_events_seconds') . 'DateTime'; + $todayWithoutSeconds = $this->$method($today); + $nextDateWithoutSeconds = $this->$method($nextDate); + + // Only proceed if the task should execute now + if ($nextDateWithoutSeconds > $todayWithoutSeconds) { + return; + } + + // Try to atomically claim this specific task + $claimed = $this->claimTask($task->id, $todayFormatted); + + if (!$claimed) { + // Another process already claimed this task, skip it + return; + } + + // Re-fetch the task to get fresh data after claiming + $task = ScheduledTask::find($task->id); + if (!$task) { + return; + } + + // Execute the task + $this->executeTask($task, $config, $todayFormatted); + + } catch (\Throwable $ex) { + Log::error('Failed Scheduled Task: ', [ + 'Task data' => print_r($task->getAttributes(), true), + 'Exception' => $ex->__toString(), + ]); + // Release task on error so it can be retried + $this->releaseTask($task); + } + } + + /** + * Atomically claim a single task for execution. + * Uses UPDATE with WHERE to ensure only one process can claim it. + * + * @param int $taskId The task ID to claim + * @param string $todayFormatted Current datetime formatted + * @return bool True if successfully claimed, false if already claimed by another process + */ + private function claimTask(int $taskId, string $todayFormatted): bool + { + $claimId = Str::uuid()->toString(); + + $affected = DB::table('scheduled_tasks') + ->where('id', $taskId) + ->whereNull('claimed_by') + ->update([ + 'claimed_by' => $claimId, + 'claimed_at' => $todayFormatted, + ]); + + return $affected > 0; + } + + /** + * Execute a task based on its type. + * + * @param ScheduledTask $task The task to execute + * @param object $config Task configuration + * @param string $todayFormatted Formatted datetime for last_execution + */ + private function executeTask(ScheduledTask $task, object $config, string $todayFormatted): void + { + $executed = false; + + switch ($task->type) { + case 'TIMER_START_EVENT': + $this->executeTimerStartEvent($task, $config); + $executed = true; + break; + case 'INTERMEDIATE_TIMER_EVENT': + $executed = $this->executeIntermediateTimerEvent($task, $config); + break; + case 'BOUNDARY_TIMER_EVENT': + $executed = $this->executeBoundaryTimerEvent($task, $config); + break; + case 'SCHEDULED_JOB': + $this->executeScheduledJob($config); + $executed = true; + break; + default: + throw new Exception('Unknown timer event: ' . $task->type); + } + + if ($executed) { + // Update last_execution and release claim + $task->last_execution = $todayFormatted; + $task->claimed_by = null; + $task->claimed_at = null; + $task->save(); + } else { + // Release claim without updating last_execution + $this->releaseTask($task); + } + } + + /** + * Release a task claim without updating last_execution. + * + * @param ScheduledTask $task The task to release + */ + private function releaseTask(ScheduledTask $task): void + { + $task->claimed_by = null; + $task->claimed_at = null; + $task->save(); + } + /** * Create a scheduled job * diff --git a/ProcessMaker/Models/ScheduledTask.php b/ProcessMaker/Models/ScheduledTask.php index b5d3e1b481..c3e2b512be 100644 --- a/ProcessMaker/Models/ScheduledTask.php +++ b/ProcessMaker/Models/ScheduledTask.php @@ -15,6 +15,11 @@ class ScheduledTask extends ProcessMakerModel protected $fillable = [ 'process_id', 'process_request_id', 'process_request_token_id', 'configuration', + 'type', 'last_execution', 'claimed_by', 'claimed_at', + ]; + + protected $casts = [ + 'claimed_at' => 'datetime', ]; public static function rules() diff --git a/database/migrations/2026_01_12_000000_add_claim_fields_to_scheduled_tasks_table.php b/database/migrations/2026_01_12_000000_add_claim_fields_to_scheduled_tasks_table.php new file mode 100644 index 0000000000..6627e4b657 --- /dev/null +++ b/database/migrations/2026_01_12_000000_add_claim_fields_to_scheduled_tasks_table.php @@ -0,0 +1,37 @@ +string('claimed_by', 36)->nullable()->after('configuration'); + $table->dateTime('claimed_at')->nullable()->after('claimed_by'); + + // Index for faster queries when claiming tasks + $table->index(['claimed_by', 'claimed_at']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('scheduled_tasks', function (Blueprint $table) { + $table->dropIndex(['claimed_by', 'claimed_at']); + $table->dropColumn(['claimed_by', 'claimed_at']); + }); + } +}; diff --git a/tests/Feature/ScheduledTaskDuplicationTest.php b/tests/Feature/ScheduledTaskDuplicationTest.php new file mode 100644 index 0000000000..a3dd928051 --- /dev/null +++ b/tests/Feature/ScheduledTaskDuplicationTest.php @@ -0,0 +1,418 @@ +create(['status' => 'ACTIVE']); + + // Create a scheduled task that should be executed + $task = ScheduledTask::create([ + 'process_id' => $process->id, + 'type' => 'TIMER_START_EVENT', + 'last_execution' => Carbon::now()->subMinutes(5)->format('Y-m-d H:i:s'), + 'configuration' => json_encode([ + 'type' => 'TimeCycle', + 'interval' => $this->createCycleInterval(), + 'element_id' => 'node_1', + ]), + ]); + + $now = Carbon::now()->format('Y-m-d H:i:s'); + + // First claim should succeed + $claimed1 = DB::table('scheduled_tasks') + ->where('id', $task->id) + ->whereNull('claimed_by') + ->update([ + 'claimed_by' => Str::uuid()->toString(), + 'claimed_at' => $now, + ]); + + // Second claim should fail (task already claimed) + $claimed2 = DB::table('scheduled_tasks') + ->where('id', $task->id) + ->whereNull('claimed_by') + ->update([ + 'claimed_by' => Str::uuid()->toString(), + 'claimed_at' => $now, + ]); + + $this->assertEquals(1, $claimed1, 'First claim should succeed'); + $this->assertEquals(0, $claimed2, 'Second claim should fail'); + } + + /** + * Test that stale claims are released after timeout. + */ + public function testStaleClaimsAreReleased() + { + $process = Process::factory()->create(['status' => 'ACTIVE']); + + // Create a task with a stale claim (claimed 10 minutes ago) + $task = ScheduledTask::create([ + 'process_id' => $process->id, + 'type' => 'TIMER_START_EVENT', + 'last_execution' => Carbon::now()->subMinutes(15)->format('Y-m-d H:i:s'), + 'configuration' => json_encode([ + 'type' => 'TimeCycle', + 'interval' => $this->createCycleInterval(), + 'element_id' => 'node_1', + ]), + 'claimed_by' => 'stale-claim-id', + 'claimed_at' => Carbon::now()->subMinutes(10), + ]); + + $manager = new TaskSchedulerManager(); + + // Use reflection to call the private method + $reflection = new \ReflectionClass($manager); + $method = $reflection->getMethod('releaseStaleClaimedTasks'); + $method->setAccessible(true); + $method->invoke($manager); + + // Verify the claim was released + $task->refresh(); + $this->assertNull($task->claimed_by); + $this->assertNull($task->claimed_at); + } + + /** + * Test that recent claims are NOT released. + */ + public function testRecentClaimsAreNotReleased() + { + $process = Process::factory()->create(['status' => 'ACTIVE']); + + $claimId = Str::uuid()->toString(); + + // Create a task with a recent claim (1 minute ago) + $task = ScheduledTask::create([ + 'process_id' => $process->id, + 'type' => 'TIMER_START_EVENT', + 'last_execution' => Carbon::now()->subMinutes(5)->format('Y-m-d H:i:s'), + 'configuration' => json_encode([ + 'type' => 'TimeCycle', + 'interval' => $this->createCycleInterval(), + 'element_id' => 'node_1', + ]), + 'claimed_by' => $claimId, + 'claimed_at' => Carbon::now()->subMinute(), + ]); + + $manager = new TaskSchedulerManager(); + + $reflection = new \ReflectionClass($manager); + $method = $reflection->getMethod('releaseStaleClaimedTasks'); + $method->setAccessible(true); + $method->invoke($manager); + + // Verify the claim was NOT released + $task->refresh(); + $this->assertEquals($claimId, $task->claimed_by); + $this->assertNotNull($task->claimed_at); + } + + /** + * Test that claimTask method works correctly. + */ + public function testClaimTaskMethod() + { + $process = Process::factory()->create(['status' => 'ACTIVE']); + + $task = ScheduledTask::create([ + 'process_id' => $process->id, + 'type' => 'TIMER_START_EVENT', + 'last_execution' => Carbon::now()->subMinutes(5)->format('Y-m-d H:i:s'), + 'configuration' => json_encode([ + 'type' => 'TimeCycle', + 'interval' => $this->createCycleInterval(), + 'element_id' => 'node_1', + ]), + ]); + + $manager = new TaskSchedulerManager(); + $now = Carbon::now()->format('Y-m-d H:i:s'); + + // Use reflection to call the private method + $reflection = new \ReflectionClass($manager); + $method = $reflection->getMethod('claimTask'); + $method->setAccessible(true); + + // First claim should succeed + $result1 = $method->invoke($manager, $task->id, $now); + $this->assertTrue($result1, 'First claim should succeed'); + + // Second claim should fail + $result2 = $method->invoke($manager, $task->id, $now); + $this->assertFalse($result2, 'Second claim should fail'); + } + + /** + * Test race condition prevention with multiple concurrent claims. + */ + public function testRaceConditionPrevention() + { + $process = Process::factory()->create(['status' => 'ACTIVE']); + + $task = ScheduledTask::create([ + 'process_id' => $process->id, + 'type' => 'TIMER_START_EVENT', + 'last_execution' => Carbon::now()->subMinutes(5)->format('Y-m-d H:i:s'), + 'configuration' => json_encode([ + 'type' => 'TimeCycle', + 'interval' => $this->createCycleInterval(), + 'element_id' => 'node_1', + ]), + ]); + + $claimResults = []; + $now = Carbon::now()->format('Y-m-d H:i:s'); + + // Simulate 10 concurrent claims + for ($i = 0; $i < 10; $i++) { + $claimed = DB::table('scheduled_tasks') + ->where('id', $task->id) + ->whereNull('claimed_by') + ->update([ + 'claimed_by' => Str::uuid()->toString(), + 'claimed_at' => $now, + ]); + + $claimResults[] = $claimed; + } + + // Only ONE claim should succeed + $successfulClaims = array_sum($claimResults); + $this->assertEquals(1, $successfulClaims, 'Only one claim should succeed out of 10 attempts'); + } + + /** + * Test that task is released after successful execution. + */ + public function testTaskReleasedAfterExecution() + { + $process = Process::factory()->create(['status' => 'ACTIVE']); + + $task = ScheduledTask::create([ + 'process_id' => $process->id, + 'type' => 'TIMER_START_EVENT', + 'last_execution' => Carbon::now()->subMinutes(5)->format('Y-m-d H:i:s'), + 'configuration' => json_encode([ + 'type' => 'TimeCycle', + 'interval' => $this->createCycleInterval(), + 'element_id' => 'node_1', + ]), + 'claimed_by' => 'test-claim-id', + 'claimed_at' => Carbon::now(), + ]); + + $manager = new TaskSchedulerManager(); + + $reflection = new \ReflectionClass($manager); + $method = $reflection->getMethod('releaseTask'); + $method->setAccessible(true); + $method->invoke($manager, $task); + + // Verify the task was released + $task->refresh(); + $this->assertNull($task->claimed_by); + $this->assertNull($task->claimed_at); + } + + /** + * Test that already claimed tasks are skipped during processing. + */ + public function testAlreadyClaimedTasksAreSkipped() + { + $process = Process::factory()->create(['status' => 'ACTIVE']); + + // Task that is already claimed by another process + $claimedTask = ScheduledTask::create([ + 'process_id' => $process->id, + 'type' => 'TIMER_START_EVENT', + 'last_execution' => Carbon::now()->subMinutes(5)->format('Y-m-d H:i:s'), + 'configuration' => json_encode([ + 'type' => 'TimeCycle', + 'interval' => $this->createCycleInterval(), + 'element_id' => 'node_1', + ]), + 'claimed_by' => 'other-process-claim', + 'claimed_at' => Carbon::now(), + ]); + + // Unclaimed task + $unclaimedTask = ScheduledTask::create([ + 'process_id' => $process->id, + 'type' => 'TIMER_START_EVENT', + 'last_execution' => Carbon::now()->subMinutes(5)->format('Y-m-d H:i:s'), + 'configuration' => json_encode([ + 'type' => 'TimeCycle', + 'interval' => $this->createCycleInterval(), + 'element_id' => 'node_2', + ]), + ]); + + // Query for unclaimed tasks only (as the new implementation does) + $unclaimedTasks = ScheduledTask::whereNull('claimed_by')->get(); + + $this->assertCount(1, $unclaimedTasks); + $this->assertEquals($unclaimedTask->id, $unclaimedTasks->first()->id); + } + + /** + * Test selection logic: task should NOT be claimed if nextDate is in the future. + */ + public function testTaskNotExecutedIfNextDateInFuture() + { + $process = Process::factory()->create(['status' => 'ACTIVE']); + + // Set a fake "today" to control time precisely + $fakeToday = Carbon::create(2026, 1, 12, 12, 0, 0, 'UTC'); + TaskSchedulerManager::fakeToday($fakeToday); + + // Create a task that was executed 5 minutes ago with 60-minute interval + // Next execution should be at 12:55 (55 minutes in the future) + $task = ScheduledTask::create([ + 'process_id' => $process->id, + 'type' => 'TIMER_START_EVENT', + 'last_execution' => Carbon::create(2026, 1, 12, 11, 55, 0, 'UTC')->format('Y-m-d H:i:s'), + 'configuration' => json_encode([ + 'type' => 'TimeCycle', + 'interval' => $this->createCycleIntervalFromDate( + Carbon::create(2026, 1, 12, 10, 55, 0, 'UTC'), // Start at 10:55 + 60 // Every 60 minutes + ), + 'element_id' => 'node_1', + ]), + ]); + + $manager = new TaskSchedulerManager(); + $today = $manager->today(); + + // Calculate nextDate using the manager + $config = json_decode($task->configuration); + $lastExecution = new \DateTime($task->last_execution, new \DateTimeZone('UTC')); + $nextDate = $manager->nextDate($today, $config, $lastExecution, null); + + // nextDate should be 12:55 which is 55 minutes after "today" (12:00) + $this->assertNotNull($nextDate); + $this->assertGreaterThan($today->getTimestamp(), $nextDate->getTimestamp()); + + // Reset fake today + TaskSchedulerManager::fakeToday(null); + } + + /** + * Test selection logic: task SHOULD be executed if nextDate has passed. + */ + public function testTaskExecutedIfNextDatePassed() + { + $process = Process::factory()->create(['status' => 'ACTIVE']); + + // Set a fake "today" to control time precisely + $fakeToday = Carbon::create(2026, 1, 12, 12, 30, 0, 'UTC'); + TaskSchedulerManager::fakeToday($fakeToday); + + // Create a task with last execution at 12:00, 1-minute interval + // Next execution should be at 12:01, which is 29 minutes in the past + $task = ScheduledTask::create([ + 'process_id' => $process->id, + 'type' => 'TIMER_START_EVENT', + 'last_execution' => Carbon::create(2026, 1, 12, 12, 0, 0, 'UTC')->format('Y-m-d H:i:s'), + 'configuration' => json_encode([ + 'type' => 'TimeCycle', + 'interval' => $this->createCycleIntervalFromDate( + Carbon::create(2026, 1, 12, 11, 0, 0, 'UTC'), // Start at 11:00 + 1 // Every 1 minute + ), + 'element_id' => 'node_1', + ]), + ]); + + $manager = new TaskSchedulerManager(); + $today = $manager->today(); + + // Calculate nextDate + $config = json_decode($task->configuration); + $lastExecution = new \DateTime($task->last_execution, new \DateTimeZone('UTC')); + $nextDate = $manager->nextDate($today, $config, $lastExecution, null); + + // nextDate should be 12:01 which is before "today" (12:30) + $this->assertNotNull($nextDate); + $this->assertLessThanOrEqual($today->getTimestamp(), $nextDate->getTimestamp()); + + // Reset fake today + TaskSchedulerManager::fakeToday(null); + } + + /** + * Helper method to create a cycle interval configuration. + * + * @param int $minutes Interval in minutes (default 1) + */ + private function createCycleInterval(int $minutes = 1): object + { + return (object) [ + 'start' => (object) [ + 'date' => Carbon::now()->subHour()->format('Y-m-d H:i:s'), + 'timezone' => 'UTC', + ], + 'interval' => (object) [ + 'y' => 0, + 'm' => 0, + 'd' => 0, + 'h' => 0, + 'i' => $minutes, + 's' => 0, + 'f' => 0, + ], + 'end' => null, + 'recurrences' => 0, + ]; + } + + /** + * Helper method to create a cycle interval configuration with a specific start date. + * + * @param Carbon $startDate The start date for the cycle + * @param int $minutes Interval in minutes + */ + private function createCycleIntervalFromDate(Carbon $startDate, int $minutes): object + { + return (object) [ + 'start' => (object) [ + 'date' => $startDate->format('Y-m-d H:i:s'), + 'timezone' => 'UTC', + ], + 'interval' => (object) [ + 'y' => 0, + 'm' => 0, + 'd' => 0, + 'h' => 0, + 'i' => $minutes, + 's' => 0, + 'f' => 0, + ], + 'end' => null, + 'recurrences' => 0, + ]; + } +} From 99f72ef910291eb9574647a033a5d6fab11b10b8 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Tue, 13 Jan 2026 16:45:37 -0400 Subject: [PATCH 2/2] Enhance task scheduling with overlapping prevention - Updated the bpmn:timer command in Kernel.php to prevent overlapping executions by adding the withoutOverlapping method with a 5-minute timeout. --- ProcessMaker/Console/Kernel.php | 3 ++- ProcessMaker/Managers/TaskSchedulerManager.php | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ProcessMaker/Console/Kernel.php b/ProcessMaker/Console/Kernel.php index 1d6ee38a81..44db16a0cf 100644 --- a/ProcessMaker/Console/Kernel.php +++ b/ProcessMaker/Console/Kernel.php @@ -24,7 +24,8 @@ protected function schedule(Schedule $schedule) { $schedule->command('bpmn:timer') ->everyMinute() - ->onOneServer(); + ->onOneServer() + ->withoutOverlapping(5); $schedule->command('processmaker:sync-recommendations --queue') ->daily() diff --git a/ProcessMaker/Managers/TaskSchedulerManager.php b/ProcessMaker/Managers/TaskSchedulerManager.php index cb54a8802e..0b82c942f6 100644 --- a/ProcessMaker/Managers/TaskSchedulerManager.php +++ b/ProcessMaker/Managers/TaskSchedulerManager.php @@ -238,7 +238,6 @@ private function processTaskWithAtomicClaim(ScheduledTask $task, DateTime $today // Execute the task $this->executeTask($task, $config, $todayFormatted); - } catch (\Throwable $ex) { Log::error('Failed Scheduled Task: ', [ 'Task data' => print_r($task->getAttributes(), true),