feat: add cron service to run telecart schedule tasks

This commit is contained in:
2025-12-07 16:48:00 +03:00
parent 10c1dfa5a3
commit 16a258ab68
8 changed files with 86 additions and 67 deletions

View File

@@ -7,7 +7,6 @@ services:
- "./scripts:/scripts"
- "./module:/module"
- "./build:/build"
- "/Users/nikitakiselev/code/msvlad.com/image/catalog/products:/web/upload/image/catalog/products"
ports:
- "8000:80"
restart: always
@@ -53,5 +52,20 @@ services:
- ./sql_dumps:/sql_dumps
- ./docker/mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
cron:
image: ghcr.io/telecart-labs/scheduler:latest
platform: linux/amd64
restart: unless-stopped
environment:
CRONTAB: |
*\10 * * * * php /module/oc_telegram_shop/upload/cli.php schedule:run > /proc/1/fd/1
volumes:
- ./src:/web
- ./scripts:/scripts
- ./module:/module
- ./build:/build
depends_on:
- mysql
volumes:
mysql_data:

View File

@@ -137,16 +137,16 @@
},
{
"name": "doctrine/dbal",
"version": "3.10.3",
"version": "3.10.4",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "65edaca19a752730f290ec2fb89d593cb40afb43"
"reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/65edaca19a752730f290ec2fb89d593cb40afb43",
"reference": "65edaca19a752730f290ec2fb89d593cb40afb43",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/63a46cb5aa6f60991186cc98c1d1b50c09311868",
"reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868",
"shasum": ""
},
"require": {
@@ -170,8 +170,8 @@
"phpunit/phpunit": "9.6.29",
"slevomat/coding-standard": "8.24.0",
"squizlabs/php_codesniffer": "4.0.0",
"symfony/cache": "^5.4|^6.0|^7.0",
"symfony/console": "^4.4|^5.4|^6.0|^7.0"
"symfony/cache": "^5.4|^6.0|^7.0|^8.0",
"symfony/console": "^4.4|^5.4|^6.0|^7.0|^8.0"
},
"suggest": {
"symfony/console": "For helpful console commands such as SQL execution and import of files."
@@ -231,7 +231,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/3.10.3"
"source": "https://github.com/doctrine/dbal/tree/3.10.4"
},
"funding": [
{
@@ -247,7 +247,7 @@
"type": "tidelift"
}
],
"time": "2025-10-09T09:05:12+00:00"
"time": "2025-11-29T10:46:08+00:00"
},
{
"name": "doctrine/deprecations",
@@ -3603,16 +3603,16 @@
},
{
"name": "nikic/php-parser",
"version": "v5.6.2",
"version": "v5.7.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "3a454ca033b9e06b63282ce19562e892747449bb"
"reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb",
"reference": "3a454ca033b9e06b63282ce19562e892747449bb",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
"reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
"shasum": ""
},
"require": {
@@ -3655,9 +3655,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2"
"source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
},
"time": "2025-10-21T19:32:17+00:00"
"time": "2025-12-06T11:56:16+00:00"
},
{
"name": "phar-io/manifest",
@@ -3779,11 +3779,11 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.32",
"version": "2.1.33",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/e126cad1e30a99b137b8ed75a85a676450ebb227",
"reference": "e126cad1e30a99b137b8ed75a85a676450ebb227",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f",
"reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f",
"shasum": ""
},
"require": {
@@ -3828,7 +3828,7 @@
"type": "github"
}
],
"time": "2025-11-11T15:18:17+00:00"
"time": "2025-12-05T10:24:31+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -4151,16 +4151,16 @@
},
{
"name": "phpunit/phpunit",
"version": "9.6.29",
"version": "9.6.31",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3"
"reference": "945d0b7f346a084ce5549e95289962972c4272e5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3",
"reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/945d0b7f346a084ce5549e95289962972c4272e5",
"reference": "945d0b7f346a084ce5549e95289962972c4272e5",
"shasum": ""
},
"require": {
@@ -4234,7 +4234,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29"
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.31"
},
"funding": [
{
@@ -4258,7 +4258,7 @@
"type": "tidelift"
}
],
"time": "2025-09-24T06:29:11+00:00"
"time": "2025-12-06T07:45:52+00:00"
},
{
"name": "roave/security-advisories",
@@ -4266,12 +4266,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "070af2db86d1502f430fd627a045e7b078abf63f"
"reference": "10c1e6abcb8094a428b92e7d8c3126371f9f9126"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/070af2db86d1502f430fd627a045e7b078abf63f",
"reference": "070af2db86d1502f430fd627a045e7b078abf63f",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/10c1e6abcb8094a428b92e7d8c3126371f9f9126",
"reference": "10c1e6abcb8094a428b92e7d8c3126371f9f9126",
"shasum": ""
},
"conflict": {
@@ -4283,6 +4283,7 @@
"aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2",
"aimeos/ai-admin-jsonadm": "<2020.10.13|>=2021.04.1,<2021.10.6|>=2022.04.1,<2022.10.3|>=2023.04.1,<2023.10.4|==2024.04.1",
"aimeos/ai-client-html": ">=2020.04.1,<2020.10.27|>=2021.04.1,<2021.10.22|>=2022.04.1,<2022.10.13|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.04.7",
"aimeos/ai-cms-grapesjs": ">=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.9|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.10.8|>=2025.04.1,<2025.10.2",
"aimeos/ai-controller-frontend": "<2020.10.15|>=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.8|>=2023.04.1,<2023.10.9|==2024.04.1",
"aimeos/aimeos-core": ">=2022.04.1,<2022.10.17|>=2023.04.1,<2023.10.17|>=2024.04.1,<2024.04.7",
"aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5",
@@ -4290,6 +4291,7 @@
"akaunting/akaunting": "<2.1.13",
"akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53",
"alextselegidis/easyappointments": "<1.5.2.0-beta1",
"alexusmai/laravel-file-manager": "<=3.3.1",
"alt-design/alt-redirect": "<1.6.4",
"alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1",
"amazing/media2click": ">=1,<1.3.3",
@@ -4397,7 +4399,7 @@
"contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4",
"contao/contao": ">=3,<3.5.37|>=4,<4.4.56|>=4.5,<4.13.56|>=5,<5.3.38|>=5.4.0.0-RC1-dev,<5.6.1",
"contao/core": "<3.5.39",
"contao/core-bundle": "<4.13.56|>=5,<5.3.38|>=5.4,<5.6.1",
"contao/core-bundle": "<4.13.57|>=5,<5.3.42|>=5.4,<5.6.5",
"contao/listing-bundle": ">=3,<=3.5.30|>=4,<4.4.8",
"contao/managed-edition": "<=1.5",
"corveda/phpsandbox": "<1.3.5",
@@ -4562,8 +4564,8 @@
"genix/cms": "<=1.1.11",
"georgringer/news": "<1.3.3",
"geshi/geshi": "<=1.0.9.1",
"getformwork/formwork": "<1.13.1|>=2.0.0.0-beta1,<2.0.0.0-beta4",
"getgrav/grav": "<1.7.46",
"getformwork/formwork": "<2.2",
"getgrav/grav": "<1.11.0.0-beta1",
"getkirby/cms": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1|>=5,<5.1.4",
"getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1",
"getkirby/panel": "<2.5.14",
@@ -4685,7 +4687,7 @@
"leantime/leantime": "<3.3",
"lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3",
"libreform/libreform": ">=2,<=2.0.8",
"librenms/librenms": "<2017.08.18",
"librenms/librenms": "<25.11",
"liftkit/database": "<2.13.2",
"lightsaml/lightsaml": "<1.3.5",
"limesurvey/limesurvey": "<6.5.12",
@@ -4715,8 +4717,9 @@
"marshmallow/nova-tiptap": "<5.7",
"matomo/matomo": "<1.11",
"matyhtf/framework": "<3.0.6",
"mautic/core": "<5.2.8|>=6.0.0.0-alpha,<6.0.5",
"mautic/core": "<5.2.9|>=6,<6.0.7",
"mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1",
"mautic/grapes-js-builder-bundle": ">=4,<4.4.18|>=5,<5.2.9|>=6,<6.0.7",
"maximebf/debugbar": "<1.19",
"mdanter/ecc": "<2",
"mediawiki/abuse-filter": "<1.39.9|>=1.40,<1.41.3|>=1.42,<1.42.2",
@@ -4844,6 +4847,7 @@
"phpoffice/math": "<=0.2",
"phpoffice/phpexcel": "<=1.8.2",
"phpoffice/phpspreadsheet": "<1.30|>=2,<2.1.12|>=2.2,<2.4|>=3,<3.10|>=4,<5",
"phppgadmin/phppgadmin": "<=7.13",
"phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36",
"phpservermon/phpservermon": "<3.6",
"phpsysinfo/phpsysinfo": "<3.4.3",
@@ -4901,7 +4905,7 @@
"rap2hpoutre/laravel-log-viewer": "<0.13",
"react/http": ">=0.7,<1.9",
"really-simple-plugins/complianz-gdpr": "<6.4.2",
"redaxo/source": "<5.18.3",
"redaxo/source": "<5.20.1",
"remdex/livehelperchat": "<4.29",
"renolit/reint-downloadmanager": "<4.0.2|>=5,<5.0.1",
"reportico-web/reportico": "<=8.1",
@@ -4968,7 +4972,7 @@
"slim/slim": "<2.6",
"slub/slub-events": "<3.0.3",
"smarty/smarty": "<4.5.3|>=5,<5.1.1",
"snipe/snipe-it": "<8.1.18",
"snipe/snipe-it": "<=8.3.4",
"socalnick/scn-social-auth": "<1.15.2",
"socialiteproviders/steam": "<1.1",
"solspace/craft-freeform": ">=5,<5.10.16",
@@ -5253,7 +5257,7 @@
"type": "tidelift"
}
],
"time": "2025-11-19T21:05:11+00:00"
"time": "2025-12-05T21:05:14+00:00"
},
{
"name": "sebastian/cli-parser",

View File

@@ -2,7 +2,8 @@
namespace Console\Commands;
use Openguru\OpenCartFramework\Config\SettingsInterface;
use Carbon\Carbon;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Scheduler\SchedulerService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -11,12 +12,12 @@ use Symfony\Component\Console\Output\OutputInterface;
class ScheduleRunCommand extends TeleCartCommand
{
private SchedulerService $scheduler;
private SettingsInterface $settings;
private Settings $settings;
protected static $defaultName = 'schedule:run';
protected static $defaultDescription = 'Run scheduled commands';
public function __construct(SchedulerService $scheduler, SettingsInterface $settings)
public function __construct(SchedulerService $scheduler, Settings $settings)
{
parent::__construct();
$this->scheduler = $scheduler;
@@ -28,10 +29,14 @@ class ScheduleRunCommand extends TeleCartCommand
$mode = $this->settings->get('cron.mode', 'disabled');
if ($mode !== 'system') {
$output->writeln('<comment>Scheduler is disabled. Skipping CLI execution.</comment>');
return Command::SUCCESS;
}
$output->writeln('<info>TeleCart Scheduler Running...</info>');
$output->writeln(sprintf(
'[%s] <info>TeleCart Scheduler Running...</info>',
Carbon::now()->toJSON(),
));
$result = $this->scheduler->run();

View File

@@ -4,7 +4,7 @@ namespace Openguru\OpenCartFramework\Config;
use Openguru\OpenCartFramework\Support\Arr;
class Settings implements SettingsInterface
class Settings
{
private array $config;

View File

@@ -1,14 +0,0 @@
<?php
namespace Openguru\OpenCartFramework\Config;
interface SettingsInterface
{
public function get(string $key, $default = null);
public function has(string $key): bool;
public function getAll(): array;
public function getHash(): string;
}

View File

@@ -4,7 +4,7 @@ namespace Openguru\OpenCartFramework\Scheduler;
use DateTime;
use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\Config\SettingsInterface;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Container\Container;
use Psr\Log\LoggerInterface;
use Throwable;
@@ -14,14 +14,18 @@ class SchedulerService
private LoggerInterface $logger;
private CacheInterface $cache;
private Container $container;
private SettingsInterface $settings;
private Settings $settings;
private ?ScheduleJobRegistry $registry = null;
private const GLOBAL_LOCK_KEY = 'scheduler.global_lock';
private const GLOBAL_LOCK_TTL = 300; // 5 minutes
public function __construct(LoggerInterface $logger, CacheInterface $cache, Container $container, SettingsInterface $settings)
{
public function __construct(
LoggerInterface $logger,
CacheInterface $cache,
Container $container,
Settings $settings
) {
$this->logger = $logger;
$this->cache = $cache;
$this->container = $container;
@@ -41,11 +45,13 @@ class SchedulerService
$mode = $this->settings->get('cron.mode', 'disabled');
if ($mode === 'disabled') {
$result->addSkipped('Global', 'Scheduler is disabled');
return $result;
}
if ($this->isGlobalLocked()) {
$result->addSkipped('Global', 'Global scheduler lock active');
return $result;
}
@@ -60,7 +66,7 @@ class SchedulerService
$scheduler = $this->registry ?: new ScheduleJobRegistry($this->container);
// Only load config file if registry was not injected (for production use)
if (!$this->registry) {
if (! $this->registry) {
$configFile = BP_BASE_PATH . '/configs/schedule.php';
if (file_exists($configFile)) {
require $configFile;
@@ -91,18 +97,21 @@ class SchedulerService
// 1. Check if due by Cron expression
if (! $job->isDue()) {
$result->addSkipped($name, 'Not due');
return;
}
// 2. Check Last Run (Prevent running multiple times in the same minute)
if ($this->hasRanRecently($id)) {
$result->addSkipped($name, 'Already ran recently');
return;
}
// 3. Check Lock (Prevent parallel execution)
if ($this->isJobLocked($id)) {
$result->addSkipped($name, 'Job is locked (running)');
return;
}
@@ -169,7 +178,7 @@ class SchedulerService
return false;
}
$lastRunDate = (new DateTime())->setTimestamp((int)$lastRun);
$lastRunDate = (new DateTime())->setTimestamp((int) $lastRun);
$now = new DateTime();
return $lastRunDate->format('Y-m-d H:i') === $now->format('Y-m-d H:i');
@@ -188,7 +197,8 @@ class SchedulerService
public function getGlobalLastRun(): ?int
{
$time = $this->cache->get("scheduler.global_last_run");
return $time ? (int)$time : null;
return $time ? (int) $time : null;
}
private function updateLastFailure(string $id, string $message): void
@@ -205,7 +215,7 @@ class SchedulerService
public function getLastFailure(string $id): ?array
{
$time = $this->cache->get("scheduler.last_failure.{$id}");
if (!$time) {
if (! $time) {
return null;
}

View File

@@ -3,7 +3,7 @@
namespace Openguru\OpenCartFramework\Scheduler;
use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\Config\SettingsInterface;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Container\Container;
use Openguru\OpenCartFramework\Container\ServiceProvider;
use Psr\Log\LoggerInterface;
@@ -17,7 +17,7 @@ class SchedulerServiceProvider extends ServiceProvider
$container->get(LoggerInterface::class),
$container->get(CacheInterface::class),
$container,
$container->get(SettingsInterface::class)
$container->get(Settings::class)
);
});
}

View File

@@ -4,7 +4,7 @@ namespace Tests\Unit\Framework\Scheduler;
use Mockery;
use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\Config\SettingsInterface;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Scheduler\Job;
use Openguru\OpenCartFramework\Scheduler\SchedulerService;
use Openguru\OpenCartFramework\Scheduler\ScheduleJobRegistry;
@@ -23,7 +23,7 @@ class SchedulerServiceTest extends TestCase
parent::setUp();
$this->cacheMock = Mockery::mock(CacheInterface::class);
$this->settingsMock = Mockery::mock(SettingsInterface::class);
$this->settingsMock = Mockery::mock(Settings::class);
$this->loggerMock = Mockery::mock(LoggerInterface::class);
$this->scheduler = new SchedulerService(