mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-09-23 11:15:02 -04:00
feat(jobs): add task time warning threshold setting
Introduce a soft, log-only per-task time budget (jobs_task_max_seconds) separate from the existing web request budget (jobs_web_max_seconds), since PHP cannot preempt a running task mid-execution. - Config: add taskMaxSeconds default (30s) to app/Config/Jobs.php - Controller: validate and persist task_max_seconds in Jobs::postSaveSettings - BoundedTaskRunner: accept taskMaxSeconds, measure task runtime via microtime, log a warning when a task exceeds the threshold - JobRunner: read jobs_task_max_seconds from config and pass it to BoundedTaskRunner - Migration: add AddJobsTaskMaxSecondsConfigKey to seed default app_config value - Language: add en strings for label, validation message, and enabled/disabled tooltips - View: add task_max_seconds form field to settings_config.php, wired to same web-mode enable/disable toggle as web_max_seconds - Tests: update JobsControllerTest to cover new field in save/ validation scenarios Signed-off-by: objecttothis <17935339+objecttothis@users.noreply.github.com>
This commit is contained in:
1 parent
1e42c3b7ef
commit
14b06eea30
8 files changed
+97
-15
No files matched your search
@@ -8,4 +8,5 @@ class Jobs extends BaseConfig
|
||||
{
|
||||
public string $mode = 'web'; // auto | web | manual
|
||||
public int $webMaxSeconds = 5;
|
||||
public int $taskMaxSeconds = 30;
|
||||
}
|
||||
@@ -48,13 +48,15 @@ class Jobs extends Secure_Controller
|
||||
public function postSaveSettings(): ResponseInterface
|
||||
{
|
||||
$rules = [
|
||||
'mode' => 'required|in_list[auto,web,manual]',
|
||||
'web_max_seconds' => 'required|is_natural'
|
||||
'mode' => 'required|in_list[auto,web,manual]',
|
||||
'web_max_seconds' => 'required|is_natural',
|
||||
'task_max_seconds' => 'required|is_natural'
|
||||
];
|
||||
|
||||
$messages = [
|
||||
'mode' => ['in_list' => lang('Jobs.mode_invalid')],
|
||||
'web_max_seconds' => ['is_natural' => lang('Jobs.web_max_seconds_invalid')]
|
||||
'mode' => ['in_list' => lang('Jobs.mode_invalid')],
|
||||
'web_max_seconds' => ['is_natural' => lang('Jobs.web_max_seconds_invalid')],
|
||||
'task_max_seconds' => ['is_natural' => lang('Jobs.task_max_seconds_invalid')]
|
||||
];
|
||||
|
||||
if ($response = $this->validateFields($rules, $messages)) {
|
||||
@@ -62,8 +64,9 @@ class Jobs extends Secure_Controller
|
||||
}
|
||||
|
||||
$batchSaveData = [
|
||||
'jobs_mode' => $this->request->getPost('mode'),
|
||||
'jobs_web_max_seconds' => $this->request->getPost('web_max_seconds', FILTER_SANITIZE_NUMBER_INT)
|
||||
'jobs_mode' => $this->request->getPost('mode'),
|
||||
'jobs_web_max_seconds' => $this->request->getPost('web_max_seconds', FILTER_SANITIZE_NUMBER_INT),
|
||||
'jobs_task_max_seconds' => $this->request->getPost('task_max_seconds', FILTER_SANITIZE_NUMBER_INT)
|
||||
];
|
||||
|
||||
$success = $this->appconfig->batch_save($batchSaveData);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddJobsTaskMaxSecondsConfigKey extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->db->table('app_config')->ignore(true)->insert(['key' => 'jobs_task_max_seconds', 'value' => '30']);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->db->table('app_config')->where('key', 'jobs_task_max_seconds')->delete();
|
||||
}
|
||||
}
|
||||
@@ -15,16 +15,22 @@ use Throwable;
|
||||
* mid-execution (PHP has no portable, FPM-safe preemption mechanism), so
|
||||
* this only bounds how many *additional* tasks are started after the
|
||||
* deadline, not the runtime of a task that was already running.
|
||||
*
|
||||
* $taskMaxSeconds is a soft, log-only budget: if a single task's run()
|
||||
* takes longer than this, a warning is logged after the fact. It cannot
|
||||
* stop or interrupt the task for the same preemption reason above.
|
||||
*/
|
||||
class BoundedTaskRunner extends TaskRunner
|
||||
{
|
||||
private float $deadline;
|
||||
private float $taskMaxSeconds;
|
||||
|
||||
public function __construct(float $deadline)
|
||||
public function __construct(float $deadline, float $taskMaxSeconds)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->deadline = $deadline;
|
||||
$this->taskMaxSeconds = $taskMaxSeconds;
|
||||
}
|
||||
|
||||
public function run()
|
||||
@@ -51,6 +57,7 @@ class BoundedTaskRunner extends TaskRunner
|
||||
|
||||
$error = null;
|
||||
$start = Time::now();
|
||||
$startMicrotime = microtime(true);
|
||||
$output = null;
|
||||
|
||||
$this->cliWrite('Processing: ' . ($task->name ?: 'Task'), 'green');
|
||||
@@ -58,6 +65,17 @@ class BoundedTaskRunner extends TaskRunner
|
||||
try {
|
||||
$output = $task->run();
|
||||
|
||||
$elapsed = microtime(true) - $startMicrotime;
|
||||
|
||||
if ($elapsed > $this->taskMaxSeconds) {
|
||||
log_message('warning', sprintf(
|
||||
'JobRunner: task "%s" took %.2fs, exceeding jobs_task_max_seconds (%.2fs).',
|
||||
$task->name ?: 'Task',
|
||||
$elapsed,
|
||||
$this->taskMaxSeconds
|
||||
));
|
||||
}
|
||||
|
||||
$this->cliWrite('Executed: ' . ($task->name ?: 'Task'), 'cyan');
|
||||
} catch (Throwable $e) {
|
||||
$this->cliWrite('Failed: ' . ($task->name ?: 'Task'), 'red');
|
||||
|
||||
@@ -43,8 +43,9 @@ class JobRunner implements FilterInterface
|
||||
}
|
||||
|
||||
$maxSeconds = (int)($config['jobs_web_max_seconds'] ?? config('Jobs')->webMaxSeconds);
|
||||
$taskMaxSeconds = (int)($config['jobs_task_max_seconds'] ?? config('Jobs')->taskMaxSeconds);
|
||||
|
||||
register_shutdown_function(static function () use ($maxSeconds, $lockHandle): void {
|
||||
register_shutdown_function(static function () use ($maxSeconds, $taskMaxSeconds, $lockHandle): void {
|
||||
if (function_exists('fastcgi_finish_request')) {
|
||||
fastcgi_finish_request();
|
||||
}
|
||||
@@ -55,7 +56,7 @@ class JobRunner implements FilterInterface
|
||||
config(Tasks::class)->init(service('scheduler'));
|
||||
|
||||
if (microtime(true) - $start < $maxSeconds) {
|
||||
$runner = new BoundedTaskRunner($start + $maxSeconds);
|
||||
$runner = new BoundedTaskRunner($start + $maxSeconds, $taskMaxSeconds);
|
||||
$runner->run();
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
|
||||
@@ -16,6 +16,10 @@ return [
|
||||
'select_jobs' => 'Select Jobs',
|
||||
'settings' => 'Settings',
|
||||
'settings_configuration' => 'Job Queue Settings',
|
||||
'task_max_seconds' => 'Task Time Warning Threshold',
|
||||
'task_max_seconds_invalid' => 'Task Time Warning Threshold is required and must be a non-negative integer.',
|
||||
'task_max_seconds_tooltip' => 'If a single task runs longer than this many seconds, a warning is logged. Does not stop or limit the task.',
|
||||
'task_max_seconds_tooltip_disabled' => 'Task Time Warning Threshold is only available when Mode is set to Web (no cron required).',
|
||||
'throttle_count' => 'Count',
|
||||
'throttle_count_required' => 'Throttle count is required and must be a non-negative integer.',
|
||||
'throttle_period' => 'Period',
|
||||
|
||||
@@ -55,6 +55,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group-sm">
|
||||
<?= form_label(lang('Jobs.task_max_seconds'), 'task_max_seconds', ['class' => 'required control-label col-xs-4 col-sm-3 col-md-2']) ?>
|
||||
<div class="col-xs-4 col-sm-3 col-md-2">
|
||||
<div class="input-group">
|
||||
<?= form_input(array_merge([
|
||||
'type' => 'number',
|
||||
'min' => 0,
|
||||
'name' => 'task_max_seconds',
|
||||
'id' => 'task_max_seconds',
|
||||
'class' => 'form-control input-sm required digits',
|
||||
'value' => $config['jobs_task_max_seconds'] ?? 30
|
||||
], ($config['jobs_mode'] ?? 'web') !== 'web' ? ['disabled' => true] : [])) ?>
|
||||
<span class="input-group-addon input-sm">
|
||||
<span
|
||||
id="task_max_seconds_tooltip"
|
||||
class="glyphicon glyphicon-info-sign"
|
||||
data-toggle="tooltip"
|
||||
data-placement="right"
|
||||
data-tooltip-enabled="<?= esc(lang('Jobs.task_max_seconds_tooltip')) ?>"
|
||||
data-tooltip-disabled="<?= esc(lang('Jobs.task_max_seconds_tooltip_disabled')) ?>"
|
||||
title="<?= lang('Jobs.task_max_seconds_tooltip') ?>"
|
||||
></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= form_submit([
|
||||
'name' => 'submit_jobs_settings',
|
||||
'id' => 'submit_jobs_settings',
|
||||
@@ -96,10 +123,15 @@
|
||||
const toggleWebMaxSeconds = function() {
|
||||
const isWeb = $('#mode').val() === 'web';
|
||||
$('#web_max_seconds').prop('disabled', !isWeb);
|
||||
$('#task_max_seconds').prop('disabled', !isWeb);
|
||||
|
||||
const $tooltip = $('#web_max_seconds_tooltip');
|
||||
const text = isWeb ? $tooltip.attr('data-tooltip-enabled') : $tooltip.attr('data-tooltip-disabled');
|
||||
$tooltip.attr('data-original-title', text);
|
||||
|
||||
const $taskTooltip = $('#task_max_seconds_tooltip');
|
||||
const taskText = isWeb ? $taskTooltip.attr('data-tooltip-enabled') : $taskTooltip.attr('data-tooltip-disabled');
|
||||
$taskTooltip.attr('data-original-title', taskText);
|
||||
};
|
||||
$('#mode').change(toggleWebMaxSeconds);
|
||||
toggleWebMaxSeconds();
|
||||
@@ -107,6 +139,7 @@
|
||||
$('#jobs_settings_form').validate($.extend(form_support.handler, {
|
||||
submitHandler: function(form) {
|
||||
$('#web_max_seconds').prop('disabled', false);
|
||||
$('#task_max_seconds').prop('disabled', false);
|
||||
$(form).ajaxSubmit({
|
||||
success: function(response) {
|
||||
$.notify({
|
||||
|
||||
@@ -61,8 +61,9 @@ class JobsControllerTest extends CIUnitTestCase
|
||||
$this->loginAsAdmin();
|
||||
|
||||
$response = $this->post('/jobs/saveSettings', [
|
||||
'mode' => 'bogus',
|
||||
'web_max_seconds' => 5,
|
||||
'mode' => 'bogus',
|
||||
'web_max_seconds' => 5,
|
||||
'task_max_seconds' => 30,
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
@@ -75,8 +76,9 @@ class JobsControllerTest extends CIUnitTestCase
|
||||
$this->loginAsAdmin();
|
||||
|
||||
$response = $this->post('/jobs/saveSettings', [
|
||||
'mode' => 'web',
|
||||
'web_max_seconds' => -5,
|
||||
'mode' => 'web',
|
||||
'web_max_seconds' => -5,
|
||||
'task_max_seconds' => 30,
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
@@ -89,8 +91,9 @@ class JobsControllerTest extends CIUnitTestCase
|
||||
$this->loginAsAdmin();
|
||||
|
||||
$response = $this->post('/jobs/saveSettings', [
|
||||
'mode' => 'manual',
|
||||
'web_max_seconds' => 10,
|
||||
'mode' => 'manual',
|
||||
'web_max_seconds' => 10,
|
||||
'task_max_seconds' => 20,
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
@@ -99,6 +102,7 @@ class JobsControllerTest extends CIUnitTestCase
|
||||
|
||||
$this->seeInDatabase('app_config', ['key' => 'jobs_mode', 'value' => 'manual']);
|
||||
$this->seeInDatabase('app_config', ['key' => 'jobs_web_max_seconds', 'value' => '10']);
|
||||
$this->seeInDatabase('app_config', ['key' => 'jobs_task_max_seconds', 'value' => '20']);
|
||||
}
|
||||
|
||||
public function testPostSaveThrottlesSavesAndDeletesMissingThrottles(): void
|
||||
|
||||
Reference in new issue
Block a user