├── .doclintrc ├── lang ├── _manifest_exclude ├── es.yml ├── id.yml ├── zh.yml ├── hr.yml ├── ar.yml ├── fi.yml ├── mi.yml ├── sl.yml ├── sv.yml ├── ru.yml ├── de.yml ├── pl.yml ├── eo.yml └── en.yml ├── phpstan.neon.dist ├── _config ├── cli.yml ├── legacy.yml ├── tests.yml ├── taskrunner.yml └── queuedjobs.yml ├── templates ├── QueuedJobsStalledJob.ss ├── QueuedJobsBrokenJobs.ss ├── QueuedJobsDefaultJob.ss ├── SitemapEntry.ss └── Symbiote │ └── QueuedJobs │ └── Controllers │ └── QueuedTaskRunner.ss ├── code-of-conduct.md ├── .tx └── config ├── .github ├── ISSUE_TEMPLATE │ ├── 3_blank_issue.md │ ├── config.yml │ ├── 2_feature_request.yml │ └── 1_bug_report.yml ├── workflows │ ├── ci.yml │ ├── keepalive.yml │ ├── merge-up.yml │ ├── add-prs-to-project.yml │ ├── tag-patch-release.yml │ └── dispatch-ci.yml └── PULL_REQUEST_TEMPLATE.md ├── src ├── Tasks │ ├── Engines │ │ ├── TaskRunnerEngine.php │ │ ├── QueueRunner.php │ │ ├── BaseRunner.php │ │ └── DoormanRunner.php │ ├── PublishItemsTask.php │ ├── DummyQueuedJob.php │ ├── DeleteAllJobsTask.php │ ├── CheckJobHealthTask.php │ ├── ProcessJobQueueTask.php │ └── CreateQueuedJobTask.php ├── Services │ ├── DefaultQueueHandler.php │ ├── JobErrorHandler.php │ ├── GearmanQueueHandler.php │ ├── ImmediateQueueHandler.php │ ├── QueuedJobHandler.php │ ├── ProcessManager.php │ ├── EmailService.php │ ├── QueuedJob.php │ └── AbstractQueuedJob.php ├── Dev │ └── State │ │ └── QueuedJobsTestState.php ├── Interfaces │ └── UserContextInterface.php ├── Cli │ └── ProcessJobQueueChildCommand.php ├── Workers │ └── JobWorker.php ├── Jobs │ ├── DeleteObjectJob.php │ ├── ScheduledExecutionJob.php │ ├── RunBuildTaskJob.php │ ├── PublishItemsJob.php │ ├── DoormanQueuedJobTask.php │ ├── CleanupJob.php │ └── GenerateGoogleSitemapJob.php ├── Extensions │ ├── MaintenanceLockExtension.php │ └── ScheduledExecutionExtension.php ├── QJUtils.php ├── DataObjects │ └── QueuedJobRule.php ├── Forms │ └── GridFieldQueuedJobExecute.php └── Controllers │ ├── QueuedTaskRunner.php │ └── QueuedJobsAdmin.php ├── phpunit.xml.dist ├── phpcs.xml.dist ├── .editorconfig ├── client └── styles │ └── task-runner.css ├── LICENSE └── composer.json /.doclintrc: -------------------------------------------------------------------------------- 1 | docs/en/ 2 | -------------------------------------------------------------------------------- /lang/_manifest_exclude: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | paths: 3 | - src 4 | -------------------------------------------------------------------------------- /_config/cli.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: queuedjobs-cli 3 | --- 4 | SilverStripe\Cli\Sake: 5 | commands: 6 | - 'Symbiote\QueuedJobs\Cli\ProcessJobQueueChildCommand' 7 | -------------------------------------------------------------------------------- /templates/QueuedJobsStalledJob.ss: -------------------------------------------------------------------------------- 1 | Hi, 2 | 3 | $Message 4 | 5 | Job ID: $JobID 6 | 7 | Log in to $Site to see further details and take any necessary actions. 8 | -------------------------------------------------------------------------------- /templates/QueuedJobsBrokenJobs.ss: -------------------------------------------------------------------------------- 1 | $Message 2 | 3 | <% loop $Jobs %> 4 | - $JobTitle (ID: $ID) 5 | <% end_loop %> 6 | 7 | Log in to $Site to see further details and take any necessary actions. 8 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | When having discussions about this module in issues or pull request please adhere to the [SilverStripe Community Code of Conduct](https://docs.silverstripe.org/en/contributing/code_of_conduct). 2 | -------------------------------------------------------------------------------- /_config/legacy.yml: -------------------------------------------------------------------------------- 1 | SilverStripe\ORM\DatabaseAdmin: 2 | classname_value_remapping: 3 | QueuedJobDescriptor: Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor 4 | QueuedJobRule: Symbiote\QueuedJobs\DataObjects\QueuedJobRule 5 | -------------------------------------------------------------------------------- /templates/QueuedJobsDefaultJob.ss: -------------------------------------------------------------------------------- 1 | Hi, 2 | 3 | $Title job not found on $Site 4 | type: $type 5 | Start Time: $startDateFormat 6 | Start Day: $startTimeString 7 | 8 | Log in to $Site to see further details and take any necessary actions. -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [o:silverstripe:p:silverstripe-queuedjobs:r:master] 5 | file_filter = lang/.yml 6 | source_file = lang/en.yml 7 | source_lang = en 8 | type = YML 9 | 10 | -------------------------------------------------------------------------------- /templates/SitemapEntry.ss: -------------------------------------------------------------------------------- 1 | 2 | $AbsoluteLink 3 | $LastEdited.Format(c) 4 | <% if ChangeFreq %>$ChangeFreq<% end_if %> 5 | <% if Priority %>$Priority<% end_if %> 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3_blank_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Blank issue 3 | about: Only for use by maintainers 4 | --- 5 | 6 | -------------------------------------------------------------------------------- /_config/tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: queuedjobstests 3 | --- 4 | SilverStripe\Core\Injector\Injector: 5 | SilverStripe\Dev\State\SapphireTestState: 6 | properties: 7 | States: 8 | queuedjobsstate: '%$Symbiote\QueuedJobs\Dev\State\QueuedJobsTestState' 9 | -------------------------------------------------------------------------------- /_config/taskrunner.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: QueuedDevelopmentAdmin 3 | After: 4 | - DevelopmentAdmin 5 | --- 6 | SilverStripe\Dev\DevelopmentAdmin: 7 | controllers: 8 | tasks: 9 | class: Symbiote\QueuedJobs\Controllers\QueuedTaskRunner 10 | description: 'See a list of build tasks to run (QueuedJobs version)' 11 | -------------------------------------------------------------------------------- /src/Tasks/Engines/TaskRunnerEngine.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests/ 5 | 6 | 7 | 8 | 9 | src/ 10 | 11 | tests/ 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | CodeSniffer ruleset for SilverStripe coding conventions. 4 | 5 | src 6 | tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/keepalive.yml: -------------------------------------------------------------------------------- 1 | name: Keepalive 2 | 3 | on: 4 | # At 2:10 PM UTC, on day 11 of the month 5 | schedule: 6 | - cron: '10 14 11 * *' 7 | workflow_dispatch: 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | keepalive: 13 | name: Keepalive 14 | # Only run cron on the silverstripe account 15 | if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule') 16 | runs-on: ubuntu-latest 17 | permissions: 18 | actions: write 19 | steps: 20 | - name: Keepalive 21 | uses: silverstripe/gha-keepalive@v1 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Security Vulnerability 4 | url: https://docs.silverstripe.org/en/contributing/issues_and_bugs/#reporting-security-issues 5 | about: ⚠️ We do not use GitHub issues to track security vulnerability reports. Click "open" on the right to see how to report security vulnerabilities. 6 | - name: Support Question 7 | url: https://www.silverstripe.org/community/ 8 | about: We use GitHub issues only to discuss bugs and new features. For support questions, please use one of the support options available in our community channels. 9 | -------------------------------------------------------------------------------- /.github/workflows/merge-up.yml: -------------------------------------------------------------------------------- 1 | name: Merge-up 2 | 3 | on: 4 | # At 5:25 PM UTC, only on Monday 5 | schedule: 6 | - cron: '25 17 * * 1' 7 | workflow_dispatch: 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | merge-up: 13 | name: Merge-up 14 | # Only run cron on the silverstripe account 15 | if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule') 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | actions: write 20 | steps: 21 | - name: Merge-up 22 | uses: silverstripe/gha-merge-up@v2 23 | -------------------------------------------------------------------------------- /src/Services/DefaultQueueHandler.php: -------------------------------------------------------------------------------- 1 | activateOnQueue(); 18 | } 19 | 20 | public function scheduleJob(QueuedJobDescriptor $job, $date) 21 | { 22 | // noop 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/add-prs-to-project.yml: -------------------------------------------------------------------------------- 1 | name: Add new PRs to github project 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - ready_for_review 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | addprtoproject: 13 | name: Add PR to GitHub Project 14 | # Only run on the silverstripe account 15 | if: github.repository_owner == 'silverstripe' 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Add PR to github project 19 | uses: silverstripe/gha-add-pr-to-project@v1 20 | with: 21 | app_id: ${{ vars.PROJECT_PERMISSIONS_APP_ID }} 22 | private_key: ${{ secrets.PROJECT_PERMISSIONS_APP_PRIVATE_KEY }} 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in this file, 2 | # please see the EditorConfig documentation: 3 | # http://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [*.{yml,js,json,css,scss,eslintrc,feature}] 17 | indent_size = 2 18 | indent_style = space 19 | 20 | [composer.json] 21 | indent_size = 4 22 | 23 | # Don't perform any clean-up on thirdparty files 24 | 25 | [thirdparty/**] 26 | trim_trailing_whitespace = false 27 | insert_final_newline = false 28 | 29 | [admin/thirdparty/**] 30 | trim_trailing_whitespace = false 31 | insert_final_newline = false 32 | -------------------------------------------------------------------------------- /.github/workflows/tag-patch-release.yml: -------------------------------------------------------------------------------- 1 | name: Tag patch release 2 | 3 | on: 4 | # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch 5 | workflow_dispatch: 6 | inputs: 7 | latest_local_sha: 8 | description: The latest local sha 9 | required: true 10 | type: string 11 | 12 | permissions: {} 13 | 14 | jobs: 15 | tagpatchrelease: 16 | name: Tag patch release 17 | # Only run cron on the silverstripe account 18 | if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule') 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: write 22 | steps: 23 | - name: Tag release 24 | uses: silverstripe/gha-tag-release@v2 25 | with: 26 | latest_local_sha: ${{ inputs.latest_local_sha }} 27 | -------------------------------------------------------------------------------- /src/Tasks/Engines/QueueRunner.php: -------------------------------------------------------------------------------- 1 | isMaintenanceLockActive()) { 19 | return; 20 | } 21 | 22 | $service = $this->getService(); 23 | 24 | $nextJob = $service->getNextPendingJob($queue); 25 | $this->logDescriptorStatus($nextJob, $queue); 26 | 27 | if ($nextJob instanceof QueuedJobDescriptor) { 28 | $service->processJobQueue($queue); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Dev/State/QueuedJobsTestState.php: -------------------------------------------------------------------------------- 1 | set(QueuedJobService::class, 'use_shutdown_function', false); 21 | } 22 | 23 | public function tearDown(SapphireTest $test) 24 | { 25 | // noop 26 | } 27 | 28 | public function setUpOnce($class) 29 | { 30 | // noop 31 | } 32 | 33 | public function tearDownOnce($class) 34 | { 35 | // noop 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Services/JobErrorHandler.php: -------------------------------------------------------------------------------- 1 | '%$GearmanService' 18 | ); 19 | 20 | /** 21 | * @var GearmanService 22 | */ 23 | public $gearmanService; 24 | 25 | /** 26 | * @param QueuedJobDescriptor $job 27 | */ 28 | public function startJobOnQueue(QueuedJobDescriptor $job) 29 | { 30 | $this->gearmanService->jobqueueExecute($job->ID); 31 | } 32 | 33 | /** 34 | * @param QueuedJobDescriptor $job 35 | * @param string $date 36 | */ 37 | public function scheduleJob(QueuedJobDescriptor $job, $date) 38 | { 39 | $this->gearmanService->sendJob('scheduled', 'jobqueueExecute', array($job->ID), strtotime($date ?? '')); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Services/ImmediateQueueHandler.php: -------------------------------------------------------------------------------- 1 | '%$' . QueuedJobService::class, 20 | ]; 21 | 22 | /** 23 | * @var QueuedJobService 24 | */ 25 | public $queuedJobService; 26 | 27 | /** 28 | * @param QueuedJobDescriptor $job 29 | */ 30 | public function startJobOnQueue(QueuedJobDescriptor $job) 31 | { 32 | $this->queuedJobService->runJob($job->ID); 33 | } 34 | 35 | /** 36 | * @param QueuedJobDescriptor $job 37 | * @param string $date 38 | */ 39 | public function scheduleJob(QueuedJobDescriptor $job, $date) 40 | { 41 | $this->queuedJobService->runJob($job->ID); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Interfaces/UserContextInterface.php: -------------------------------------------------------------------------------- 1 | getArgument('base64-task'))); 18 | if ($task) { 19 | $this->getService()->runJob($task->getDescriptor()->ID); 20 | } 21 | return Command::SUCCESS; 22 | } 23 | 24 | /** 25 | * Returns an instance of the QueuedJobService. 26 | * 27 | * @return QueuedJobService 28 | */ 29 | protected function getService() 30 | { 31 | return QueuedJobService::singleton(); 32 | } 33 | 34 | protected function configure() 35 | { 36 | $this->addArgument('base64-task', InputArgument::REQUIRED); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/dispatch-ci.yml: -------------------------------------------------------------------------------- 1 | name: Dispatch CI 2 | 3 | on: 4 | # At 5:25 PM UTC, only on Wednesday, Thursday, and Friday 5 | schedule: 6 | - cron: '25 17 * * 3,4,5' 7 | workflow_dispatch: 8 | inputs: 9 | major_type: 10 | description: 'Major branch type' 11 | required: true 12 | type: choice 13 | options: 14 | - 'dynamic' 15 | - 'current' 16 | - 'next' 17 | - 'previous' 18 | default: 'dynamic' 19 | minor_type: 20 | description: 'Minor branch type' 21 | required: true 22 | type: choice 23 | options: 24 | - 'dynamic' 25 | - 'next-minor' 26 | - 'next-patch' 27 | default: 'dynamic' 28 | 29 | permissions: {} 30 | 31 | jobs: 32 | dispatch-ci: 33 | name: Dispatch CI 34 | # Only run cron on the silverstripe account 35 | if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule') 36 | runs-on: ubuntu-latest 37 | permissions: 38 | contents: read 39 | actions: write 40 | steps: 41 | - name: Dispatch CI 42 | uses: silverstripe/gha-dispatch-ci@v1 43 | with: 44 | major_type: ${{ inputs.major_type }} 45 | minor_type: ${{ inputs.minor_type }} 46 | -------------------------------------------------------------------------------- /_config/queuedjobs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: queuedjobsettings 3 | --- 4 | SilverStripe\Core\Injector\Injector: 5 | QueueHandler: 6 | class: Symbiote\QueuedJobs\Services\DefaultQueueHandler 7 | DoormanRunner: 8 | class: Symbiote\QueuedJobs\Tasks\Engines\DoormanRunner 9 | 10 | Symbiote\QueuedJobs\Services\QueuedJobService: 11 | properties: 12 | queueHandler: '%$QueueHandler' 13 | # Change to %$DoormanRunner for async processing (requires *nix) 14 | queueRunner: '%$Symbiote\QueuedJobs\Tasks\Engines\QueueRunner' 15 | logger: '%$Psr\Log\LoggerInterface' 16 | 17 | DefaultRule: 18 | class: 'AsyncPHP\Doorman\Rule\InMemoryRule' 19 | properties: 20 | Processes: 1 21 | MinimumProcessorUsage: 0 22 | MaximumProcessorUsage: 100 23 | 24 | Symbiote\QueuedJobs\Tasks\Engines\DoormanRunner: 25 | properties: 26 | DefaultRules: 27 | DefaultRule: '%$DefaultRule' 28 | 29 | SilverStripe\SiteConfig\SiteConfig: 30 | extensions: 31 | - Symbiote\QueuedJobs\Extensions\MaintenanceLockExtension 32 | 33 | --- 34 | Name: gearman_queue_settings 35 | Only: 36 | moduleexists: gearman 37 | --- 38 | SilverStripe\Core\Injector\Injector: 39 | Symbiote\QueuedJobs\Services\GearmanQueueHandler: 40 | properties: 41 | gearmanService: '%$GearmanService' 42 | 43 | Symbiote\QueuedJobs\Workers\JobWorker: 44 | properties: 45 | queuedJobService: '%$Symbiote\QueuedJobs\Services\QueuedJobService' 46 | 47 | QueueHandler: 48 | class: GearmanQueueHandler 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Symbiote PTY LTD - www.symbiote.com.au 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the 8 | documentation and/or other materials provided with the distribution. 9 | * Neither the name of Silverstripe nor the names of its contributors may be used to endorse or promote products derived from this software 10 | without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 13 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 14 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 15 | GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 16 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 17 | OF SUCH DAMAGE. 18 | -------------------------------------------------------------------------------- /src/Workers/JobWorker.php: -------------------------------------------------------------------------------- 1 | queuedJobService->checkJobHealth(); 42 | $job = QueuedJobDescriptor::get()->byID($jobId); 43 | if ($job) { 44 | // check that we're not trying to execute something tooo soon 45 | if (strtotime($job->StartAfter ?? '') > DBDatetime::now()->getTimestamp()) { 46 | return; 47 | } 48 | 49 | $this->queuedJobService->runJob($jobId); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symbiote/silverstripe-queuedjobs", 3 | "description": "A framework for defining and running background jobs in a queued manner", 4 | "type": "silverstripe-vendormodule", 5 | "keywords": [ 6 | "silverstripe", 7 | "jobs" 8 | ], 9 | "license": "BSD-3-Clause", 10 | "authors": [ 11 | { 12 | "name": "Marcus Nyeholt", 13 | "email": "marcus@symbiote.com.au" 14 | }, 15 | { 16 | "name": "Damian Mooyman", 17 | "email": "damian@silverstripe.com" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.3", 22 | "silverstripe/framework": "^6.1", 23 | "silverstripe/admin": "^3", 24 | "asyncphp/doorman": "^5" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^11.3", 28 | "squizlabs/php_codesniffer": "^3.7", 29 | "silverstripe/documentation-lint": "^1", 30 | "silverstripe/standards": "^1", 31 | "phpstan/extension-installer": "^1.3" 32 | }, 33 | "minimum-stability": "dev", 34 | "prefer-stable": true, 35 | "extra": { 36 | "expose": [ 37 | "client/styles" 38 | ] 39 | }, 40 | "config": { 41 | "allow-plugins": { 42 | "dealerdirect/phpcodesniffer-composer-installer": true 43 | } 44 | }, 45 | "replace": { 46 | "silverstripe/queuedjobs": "self.version" 47 | }, 48 | "autoload": { 49 | "psr-4": { 50 | "Symbiote\\QueuedJobs\\": "src/", 51 | "Symbiote\\QueuedJobs\\Tests\\": "tests/" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature Request 2 | description: Submit a feature request (but only if you're planning on implementing it) 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Please only submit feature requests if you plan on implementing the feature yourself. 8 | See the [contributing code documentation](https://docs.silverstripe.org/en/contributing/code/#make-or-find-a-github-issue) for more guidelines about submitting feature requests. 9 | - type: textarea 10 | id: description 11 | attributes: 12 | label: Description 13 | description: A clear and concise description of the new feature, and why it belongs in core 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: more-info 18 | attributes: 19 | label: Additional context or points of discussion 20 | description: | 21 | *Optional: Any additional context, points of discussion, etc that might help validate and refine your idea* 22 | - type: checkboxes 23 | id: validations 24 | attributes: 25 | label: Validations 26 | description: "Before submitting the issue, please confirm the following:" 27 | options: 28 | - label: You intend to implement the feature yourself 29 | required: true 30 | - label: You have read the [contributing guide](https://docs.silverstripe.org/en/contributing/code/) 31 | required: true 32 | - label: You strongly believe this feature should be in core, rather than being its own community module 33 | required: true 34 | - label: You have checked for existing issues or pull requests related to this feature (and didn't find any) 35 | required: true 36 | -------------------------------------------------------------------------------- /src/Tasks/PublishItemsTask.php: -------------------------------------------------------------------------------- 1 | 19 | * @license BSD http://silverstripe.org/bsd-license/ 20 | */ 21 | class PublishItemsTask extends BuildTask 22 | { 23 | protected static string $commandName = 'PublishItemsTask'; 24 | 25 | protected function execute(InputInterface $input, PolyOutput $output): int 26 | { 27 | $root = $input->getOption('parent'); 28 | if (!$root) { 29 | $output->writeln('Sorry, you must provide a parent node to publish from'); 30 | } 31 | 32 | $itemExists = Page::get()->setUseCache(true)->filter('ID', $root)->exists(); 33 | 34 | if ($itemExists) { 35 | $job = new PublishItemsJob($root); 36 | singleton('Symbiote\\QueuedJobs\\Services\\QueuedJobService')->queueJob($job); 37 | } 38 | return Command::SUCCESS; 39 | } 40 | 41 | public function getOptions(): array 42 | { 43 | return [ 44 | new InputOption( 45 | 'parent', 46 | null, 47 | InputOption::VALUE_REQUIRED, 48 | 'The ID of the page you want to publish. This page and its children will be published' 49 | ), 50 | ]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Tasks/DummyQueuedJob.php: -------------------------------------------------------------------------------- 1 | startNumber = $number; 18 | $this->totalSteps = $this->startNumber; 19 | } 20 | } 21 | 22 | /** 23 | * @return string 24 | */ 25 | public function getTitle() 26 | { 27 | return 'Some test job for ' . $this->startNumber . ' seconds'; 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function getJobType() 34 | { 35 | return QueuedJob::QUEUED; 36 | } 37 | 38 | public function setup() 39 | { 40 | // just demonstrating how to get a job going... 41 | $this->totalSteps = $this->startNumber; 42 | $this->times = array(); 43 | } 44 | 45 | public function process() 46 | { 47 | $times = $this->times; 48 | // needed due to quirks with __set 49 | $time = DBDatetime::now()->Rfc2822(); 50 | $times[] = $time; 51 | $this->times = $times; 52 | 53 | $this->addMessage('Updated time to ' . $time); 54 | sleep(1); 55 | 56 | // make sure we're incrementing 57 | $this->currentStep++; 58 | 59 | // if ($this->currentStep > 1) { 60 | // $this->currentStep = 1; 61 | // } 62 | 63 | // and checking whether we're complete 64 | if ($this->currentStep >= $this->totalSteps) { 65 | $this->isComplete = true; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Tasks/DeleteAllJobsTask.php: -------------------------------------------------------------------------------- 1 | getOption('confirm')) { 37 | if (!Director::is_cli()) { 38 | $confirmText = '?confirm=1 to the URL'; 39 | } else { 40 | $confirmText = '--confirm'; 41 | } 42 | $output->writeln('Really delete ' . $jobs->count() . " jobs? Please add $confirmText to confirm."); 43 | return Command::INVALID; 44 | } 45 | 46 | $output->writeln('Deleting ' . $jobs->count() . ' jobs...'); 47 | $jobs->removeAll(); 48 | return Command::SUCCESS; 49 | } 50 | 51 | public function getOptions(): array 52 | { 53 | return [ 54 | new InputOption('confirm', null, InputOption::VALUE_NONE, 'Confirm you want to delete the jobs'), 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Jobs/DeleteObjectJob.php: -------------------------------------------------------------------------------- 1 | TargetClass = get_class($node); 27 | $this->TargetID = $node->ID; 28 | $this->currentStep = 0; 29 | $this->totalSteps = 1; 30 | } 31 | } 32 | 33 | /** 34 | * @param string $name 35 | * @return DataObject 36 | */ 37 | protected function getObject($name = 'Object') 38 | { 39 | return DataObject::get($this->TargetClass)->setUseCache(true)->byID($this->TargetID); 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getJobType() 46 | { 47 | return QueuedJob::IMMEDIATE; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getTitle() 54 | { 55 | $obj = $this->getObject(); 56 | if ($obj) { 57 | return _t(__CLASS__ . '.DELETE_OBJ2', 'Delete {title}', array('title' => $obj->Title)); 58 | } else { 59 | return _t(__CLASS__ . '.DELETE_JOB', 'Delete node'); 60 | } 61 | } 62 | 63 | public function process() 64 | { 65 | $obj = $this->getObject(); 66 | $obj->delete(); 67 | $this->currentStep = 1; 68 | $this->isComplete = true; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Extensions/MaintenanceLockExtension.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class MaintenanceLockExtension extends Extension 20 | { 21 | /** 22 | * @param FieldList $fields 23 | */ 24 | protected function updateCMSFields(FieldList $fields) 25 | { 26 | if (!QueuedJobService::config()->get('lock_file_enabled')) { 27 | return; 28 | } 29 | 30 | $fields->addFieldsToTab('Root.QueueSettings', [ 31 | $lockField = CheckboxField::create( 32 | 'MaintenanceLockEnabled', 33 | _t(__CLASS__ . '.LOCK_ENABLED', 'Maintenance Lock Enabled'), 34 | QueuedJobService::singleton()->isMaintenanceLockActive() 35 | ), 36 | ]); 37 | 38 | $lockField->setDescription( 39 | _t( 40 | __CLASS__ . '.LOCK_DESCRIPTION', 41 | 'Enable maintenance lock to prevent new queued jobs from being started' 42 | ) 43 | ); 44 | } 45 | 46 | /** 47 | * @param bool $value 48 | */ 49 | public function saveMaintenanceLockEnabled($value) 50 | { 51 | if (!QueuedJobService::config()->get('lock_file_enabled')) { 52 | return; 53 | } 54 | 55 | if ($value && !QueuedJobService::singleton()->isMaintenanceLockActive()) { 56 | QueuedJobService::singleton()->enableMaintenanceLock(); 57 | } 58 | 59 | if (!$value && QueuedJobService::singleton()->isMaintenanceLockActive()) { 60 | QueuedJobService::singleton()->disableMaintenanceLock(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | ## Description 7 | 11 | 12 | ## Manual testing steps 13 | 17 | 18 | ## Issues 19 | 23 | - # 24 | 25 | ## Pull request checklist 26 | 30 | - [ ] The target branch is correct 31 | - See [picking the right version](https://docs.silverstripe.org/en/contributing/code/#picking-the-right-version) 32 | - [ ] All commits are relevant to the purpose of the PR (e.g. no debug statements, unrelated refactoring, or arbitrary linting) 33 | - Small amounts of additional linting are usually okay, but if it makes it hard to concentrate on the relevant changes, ask for the unrelated changes to be reverted, and submitted as a separate PR. 34 | - [ ] The commit messages follow our [commit message guidelines](https://docs.silverstripe.org/en/contributing/code/#commit-messages) 35 | - [ ] The PR follows our [contribution guidelines](https://docs.silverstripe.org/en/contributing/code/) 36 | - [ ] Code changes follow our [coding conventions](https://docs.silverstripe.org/en/contributing/coding_conventions/) 37 | - [ ] This change is covered with tests (or tests aren't necessary for this change) 38 | - [ ] Any relevant User Help/Developer documentation is updated; for impactful changes, information is added to the changelog for the intended release 39 | - [ ] CI is green 40 | -------------------------------------------------------------------------------- /src/Services/QueuedJobHandler.php: -------------------------------------------------------------------------------- 1 | job = $job; 30 | $this->jobDescriptor = $jobDescriptor; 31 | } 32 | 33 | /** 34 | * @return QueuedJob 35 | */ 36 | public function getJob() 37 | { 38 | return $this->job; 39 | } 40 | 41 | /** 42 | * @return QueuedJobDescriptor 43 | */ 44 | public function getJobDescriptor() 45 | { 46 | return $this->jobDescriptor; 47 | } 48 | 49 | /** 50 | * Writes the record down to the log of the implementing handler 51 | */ 52 | protected function write(LogRecord $record): void 53 | { 54 | $this->handleBatch([$record]); 55 | } 56 | 57 | public function handleBatch(array $records): void 58 | { 59 | foreach ($records as $i => $record) { 60 | $records[$i] = $this->processRecord($records[$i]); 61 | $records[$i]['formatted'] = $this->getFormatter()->format($records[$i]); 62 | $this->job->addMessage($records[$i]['formatted'], $records[$i]['level_name'], $records[$i]['datetime']); 63 | }; 64 | $this->jobDescriptor->SavedJobMessages = serialize($this->job->getJobData()->messages); 65 | 66 | $this->jobDescriptor->write(); 67 | } 68 | 69 | /** 70 | * Ensure that exception context is retained. Similar logic to SyslogHandler. 71 | */ 72 | protected function getDefaultFormatter(): FormatterInterface 73 | { 74 | return new LineFormatter('%message% %context% %extra%'); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Tasks/Engines/BaseRunner.php: -------------------------------------------------------------------------------- 1 | Rfc2822() . '] '; 38 | } 39 | 40 | if (Director::is_cli()) { 41 | echo $prefix . $line . "\n"; 42 | } else { 43 | echo Convert::raw2xml($prefix . $line) . "
"; 44 | } 45 | } 46 | 47 | /** 48 | * Logs the status of the queued job descriptor. 49 | * 50 | * @param bool|null|QueuedJobDescriptor $descriptor 51 | * @param string $queue 52 | */ 53 | protected function logDescriptorStatus($descriptor, $queue) 54 | { 55 | if (is_null($descriptor)) { 56 | $this->writeLogLine('No new jobs on queue ' . $queue); 57 | } 58 | 59 | if ($descriptor === false) { 60 | $this->writeLogLine('Job is still running on queue ' . $queue); 61 | } 62 | 63 | if ($descriptor instanceof QueuedJobDescriptor) { 64 | $this->writeLogLine('Running ' . $descriptor->JobTitle . ' and others from queue ' . $queue . '.'); 65 | } 66 | } 67 | 68 | /** 69 | * Logs the number of current jobs per queue 70 | */ 71 | public function listJobs() 72 | { 73 | $service = $this->getService(); 74 | for ($i = 1; $i <= 3; $i++) { 75 | $jobs = $service->getJobList($i); 76 | $num = $jobs ? $jobs->Count() : 0; 77 | $this->writeLogLine('Found ' . $num . ' jobs for mode ' . $i . '.'); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Tasks/CheckJobHealthTask.php: -------------------------------------------------------------------------------- 1 | getOption('queue')); 35 | if ($queue === null) { 36 | $output->writeln('queue must be one of "immediate", "queued", or "large"'); 37 | return Command::INVALID; 38 | } 39 | $jobHealth = $this->getService()->checkJobHealth($queue); 40 | 41 | $unhealthyJobCount = 0; 42 | 43 | foreach ($jobHealth as $type => $IDs) { 44 | $count = count($IDs ?? []); 45 | $output->writeln('Detected and attempted restart on ' . $count . ' ' . $type . ' jobs'); 46 | $unhealthyJobCount = $unhealthyJobCount + $count; 47 | } 48 | 49 | if ($unhealthyJobCount > 0) { 50 | $msg = "$unhealthyJobCount jobs are unhealthy"; 51 | /** @var LoggerInterface $logger */ 52 | $Logger = Injector::inst()->get(LoggerInterface::class . '.errorhandler'); 53 | $Logger->error($msg); 54 | $output->writeln($msg); 55 | return Command::FAILURE; 56 | } 57 | 58 | $output->writeln('All jobs are healthy'); 59 | return Command::SUCCESS; 60 | } 61 | 62 | public function getOptions(): array 63 | { 64 | return [ 65 | new InputOption( 66 | 'queue', 67 | null, 68 | InputOption::VALUE_REQUIRED, 69 | 'The queue to check', 70 | 'queued', 71 | ['immediate', 'queued', 'large'] 72 | ), 73 | ]; 74 | } 75 | 76 | protected function getService() 77 | { 78 | return QueuedJobService::singleton(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/QJUtils.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class QJUtils 15 | { 16 | /** 17 | * Quote up a filter of the form 18 | * 19 | * array ("ParentID =" => 1) 20 | * 21 | * @param array $filter 22 | * @param string $join 23 | * @return string 24 | */ 25 | public function dbQuote($filter = array(), $join = " AND ") 26 | { 27 | $quoteChar = defined(DB::class . '::USE_ANSI_SQL') && DB::USE_ANSI_SQL ? '"' : ''; 28 | 29 | $string = ''; 30 | $sep = ''; 31 | 32 | foreach ($filter as $field => $value) { 33 | // first break the field up into its two components 34 | $operator = ''; 35 | if (is_string($field)) { 36 | list($field, $operator) = explode(' ', trim($field ?? '')); 37 | } 38 | 39 | $value = $this->recursiveQuote($value); 40 | 41 | if (strpos($field ?? '', '.')) { 42 | list($tb, $fl) = explode('.', $field ?? ''); 43 | $string .= $sep . $quoteChar . $tb . $quoteChar . '.' . $quoteChar . $fl . $quoteChar 44 | . " $operator " . $value; 45 | } else { 46 | if (is_numeric($field)) { 47 | $string .= $sep . $value; 48 | } else { 49 | $string .= $sep . $quoteChar . $field . $quoteChar . " $operator " . $value; 50 | } 51 | } 52 | 53 | $sep = $join; 54 | } 55 | 56 | return $string; 57 | } 58 | 59 | /** 60 | * @param mixed $val 61 | * @return string 62 | */ 63 | protected function recursiveQuote($val) 64 | { 65 | if (is_array($val)) { 66 | $return = array(); 67 | foreach ($val as $v) { 68 | $return[] = $this->recursiveQuote($v); 69 | } 70 | 71 | return '(' . implode(',', $return) . ')'; 72 | } 73 | if (is_null($val)) { 74 | $val = 'NULL'; 75 | } elseif (is_int($val)) { 76 | $val = (int) $val; 77 | } elseif (is_float($val)) { 78 | $val = (float) $val; 79 | } else { 80 | $val = "'" . Convert::raw2sql($val) . "'"; 81 | } 82 | 83 | return $val; 84 | } 85 | 86 | /** 87 | * @param string $message 88 | * @param string $status 89 | * @return string 90 | */ 91 | public function ajaxResponse($message, $status) 92 | { 93 | return json_encode(array( 94 | 'message' => $message, 95 | 'status' => $status, 96 | )); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lang/zh.yml: -------------------------------------------------------------------------------- 1 | zh: 2 | CreateQueuedJobTask: 3 | Description: '用于创建一个排队作业的任务。将排队作业的类名作为 “name” 参数传递,欲设置作业的开始时间,可传递一个可选的 “start" 参数(由 strtotime 解析)。' 4 | DeleteObjectJob: 5 | DELETE_JOB: 删除节点 6 | DELETE_OBJ2: '删除 {title}' 7 | GenerateSitemapJob: 8 | REGENERATE: '重新生成谷歌网站地图 .xml 文件' 9 | PublishItemsJob: 10 | Title: '发布下列项 {title}' 11 | QueuedJobDescriptor: 12 | PLURALNAME: 排队作业描述符 13 | SINGULARNAME: 排队作业描述符 14 | QueuedJobs: 15 | JOB_EXCEPT: '作业导致例外 %s 位于 %s 的第 %s 行' 16 | JOB_PAUSED: '作业在 %s 暂停' 17 | JOB_STALLED: '%s 次尝试后作业停滞 —— 请检查' 18 | JOB_TYPE: 工作类型 19 | JobsFieldTitle: 作业 20 | MEMORY_RELEASE: '工作正在释放内存并等待(%s 已用)' 21 | STALLED_JOB: 呆滞任务 22 | STALLED_JOB_MSG: '名为 %s 的工作似乎停滞不前。它已暂停,请登录查看' 23 | TABLE_ADDE: 已添加 24 | TABLE_MESSAGES: 消息 25 | TABLE_NUM_PROCESSED: 完成 26 | TABLE_STARTED: 已开始 27 | TABLE_START_AFTER: 开始于 28 | TABLE_STATUS: 状态 29 | TABLE_TITLE: 标题 30 | TABLE_TOTAL: 全部 31 | QueuedJobsAdmin: 32 | MENUTITLE: 作业 33 | ScheduledExecution: 34 | EXECUTE_EVERY: 执行每 35 | EXECUTE_FREE: 已调度(以首次执行的时间戳格式显示) 36 | ExecuteEveryDay: 日 37 | ExecuteEveryFortnight: 两周 38 | ExecuteEveryHour: 时 39 | ExecuteEveryMonth: 月 40 | ExecuteEveryWeek: 周 41 | ExecuteEveryYear: 年 42 | FIRST_EXECUTION: 第一次执行 43 | NEXT_RUN_DATE: 下一次运行日期 44 | ScheduleTabTitle: 日程表 45 | ScheduledExecutionJob: 46 | Title: '{title}计划执行' 47 | Symbiote\QueuedJobs\Controllers\QueuedJobsAdmin: 48 | JobsFieldTitle: 作业 49 | MENUTITLE: 作业 50 | Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor: 51 | JOB_TYPE: 工作类型 52 | PLURALNAME: 排队作业描述符 53 | SINGULARNAME: 排队作业描述符 54 | TABLE_ADDE: 已添加 55 | TABLE_FINISHED: 完成 56 | TABLE_MESSAGES: 消息 57 | TABLE_NUM_PROCESSED: 完成 58 | TABLE_STARTED: 已开始 59 | TABLE_START_AFTER: 开始于 60 | TABLE_STATUS: 状态 61 | TABLE_TITLE: 题目 62 | TABLE_TOTAL: 总数 63 | Symbiote\QueuedJobs\Extensions\ScheduledExecutionExtension: 64 | EXECUTE_EVERY: 执行每 65 | EXECUTE_FREE: 已调度(以首次执行的时间戳格式显示) 66 | ExecuteEveryDay: 日 67 | ExecuteEveryFortnight: 两周 68 | ExecuteEveryHour: 时 69 | ExecuteEveryMonth: 月 70 | ExecuteEveryWeek: 周 71 | ExecuteEveryYear: 年 72 | FIRST_EXECUTION: 第一次执行 73 | NEXT_RUN_DATE: 下一次运行日期 74 | ScheduleTabTitle: 日程表 75 | db_ExecuteEvery: 执行每 76 | Symbiote\QueuedJobs\Jobs\DeleteObjectJob: 77 | DELETE_JOB: 删除节点 78 | DELETE_OBJ2: '删除 {title}' 79 | Symbiote\QueuedJobs\Jobs\GenerateGoogleSitemapJob: 80 | REGENERATE: '重新生成谷歌网站地图 .xml 文件' 81 | Symbiote\QueuedJobs\Jobs\PublishItemsJob: 82 | Title: '发布下列项 {title}' 83 | Symbiote\QueuedJobs\Jobs\ScheduledExecutionJob: 84 | Title: '{title}计划执行' 85 | Symbiote\QueuedJobs\Services\QueuedJobService: 86 | STALLED_JOB: 呆滞任务 87 | Symbiote\QueuedJobs\Tasks\CreateQueuedJobTask: 88 | Description: '用于创建一个排队作业的任务。将排队作业的类名作为 “name” 参数传递,欲设置作业的开始时间,可传递一个可选的 “start" 参数(由 strtotime 解析)。' 89 | -------------------------------------------------------------------------------- /templates/Symbiote/QueuedJobs/Controllers/QueuedTaskRunner.ss: -------------------------------------------------------------------------------- 1 | $Header.RAW 2 | $Info.RAW 3 | 4 |
5 | 6 | 9 | 10 | 11 | 14 | 15 | 16 | 19 | 20 | 21 | 24 | 25 |
26 | <% if $Tasks.Count > 0 %> 27 |
28 | <% loop $Tasks %> 29 |
30 |
31 |

$Title

32 |
33 | $Description 34 | <% if $Help %> 35 |
36 | Display additional information 37 | $Help 38 |
39 | <% end_if %> 40 |
41 | <% if $Parameters %> 42 | Parameters: 43 | <% include SilverStripe/Dev/Parameters %> 44 | <% end_if %> 45 |
46 |
47 | <% if $TaskLink %> 48 | Run task 49 | <% end_if %> 50 | 51 | <% if $QueueLink %> 52 | Queue job 53 | <% end_if %> 54 |
55 |
56 | <% end_loop %> 57 |
58 | <% end_if %> 59 |
60 |
61 | 62 | $Footer.RAW 63 | -------------------------------------------------------------------------------- /src/Jobs/ScheduledExecutionJob.php: -------------------------------------------------------------------------------- 1 | objectID = $dataObject->ID; 29 | $this->objectType = $dataObject->ClassName; 30 | 31 | // captured so we have a unique hash generated for this job 32 | $this->timesExecuted = $timesExecuted; 33 | $this->totalSteps = 1; 34 | } 35 | } 36 | 37 | /** 38 | * @return DataObject 39 | */ 40 | public function getDataObject() 41 | { 42 | return DataObject::get($this->objectType)->setUseCache(true)->byID($this->objectID); 43 | } 44 | 45 | /** 46 | * @return string 47 | */ 48 | public function getTitle() 49 | { 50 | return _t( 51 | __CLASS__ . '.Title', 52 | 'Scheduled execution for {title}', 53 | array('title' => $this->getDataObject()->getTitle()) 54 | ); 55 | } 56 | 57 | 58 | public function setup() 59 | { 60 | } 61 | 62 | public function process() 63 | { 64 | $object = $this->getDataObject(); 65 | if ($object) { 66 | $object->onScheduledExecution(); 67 | 68 | // figure out what our rescheduled date should be 69 | $timeStr = $object->ExecuteFree; 70 | if ($object->ExecuteEvery) { 71 | $executeInterval = $object->ExecuteInterval; 72 | if (!$executeInterval || !is_numeric($executeInterval)) { 73 | $executeInterval = 1; 74 | } 75 | $timeStr = '+' . $executeInterval . ' ' . $object->ExecuteEvery; 76 | } 77 | 78 | $next = strtotime($timeStr ?? ''); 79 | if ($next > DBDatetime::now()->getTimestamp()) { 80 | // in the future 81 | $nextGen = DBDatetime::create()->setValue($next)->Rfc2822(); 82 | $nextId = QueuedJobService::singleton()->queueJob( 83 | Injector::inst()->create(ScheduledExecutionJob::class, $object, $this->timesExecuted + 1), 84 | $nextGen 85 | ); 86 | $object->ScheduledJobID = $nextId; 87 | $object->write(); 88 | } 89 | } 90 | 91 | $this->currentStep++; 92 | $this->isComplete = true; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Services/ProcessManager.php: -------------------------------------------------------------------------------- 1 | config()->get('persistent_child_process')) { 60 | // Prevent background tasks from being killed when this script finishes 61 | // this is an override for the default behaviour of killing background tasks 62 | return; 63 | } 64 | 65 | parent::__destruct(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Jobs/RunBuildTaskJob.php: -------------------------------------------------------------------------------- 1 | TaskClass = $taskClass; 38 | } 39 | 40 | if ($queryString) { 41 | $this->QueryString = $queryString; 42 | } 43 | 44 | $this->currentStep = 0; 45 | $this->totalSteps = 1; 46 | } 47 | 48 | /** 49 | * @param string (default: Object) 50 | * 51 | * @return DataObject 52 | */ 53 | protected function getObject($name = 'SilverStripe\\Core\\Object') 54 | { 55 | return DataObject::get($this->TargetClass)->setUseCache(true)->byID($this->TargetID); 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getJobType() 62 | { 63 | return QueuedJob::QUEUED; 64 | } 65 | 66 | /** 67 | * @return string 68 | */ 69 | public function getTitle() 70 | { 71 | $taskName = $this->QueryString ? ($this->TaskClass . '?' . $this->QueryString) : $this->TaskClass; 72 | return _t('RunBuildTaskJob.JOB_TITLE', 'Run BuildTask {task}', ['task' => $taskName]); 73 | } 74 | 75 | public function process() 76 | { 77 | if (!is_subclass_of($this->TaskClass, BuildTask::class)) { 78 | throw new \LogicException($this->TaskClass . ' is not a build task'); 79 | } 80 | 81 | $task = Injector::inst()->create($this->TaskClass); 82 | if (!$task->isEnabled()) { 83 | throw new \LogicException($this->TaskClass . ' is not enabled'); 84 | } 85 | 86 | $getVars = []; 87 | parse_str($this->QueryString ?? '', $getVars); 88 | $output = PolyOutput::create(PolyOutput::FORMAT_ANSI); 89 | $input = new ArrayInput($getVars); 90 | $input->setInteractive(false); 91 | $task->run($input, $output); 92 | 93 | $this->currentStep = 1; 94 | $this->isComplete = true; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Services/EmailService.php: -------------------------------------------------------------------------------- 1 | get(Email::class, 'queued_job_admin_email'); 25 | 26 | if ($queuedEmail || $queuedEmail === false) { 27 | return; 28 | } 29 | 30 | // if not set (and not explictly set to false), fallback to the admin email. 31 | Config::modify()->set( 32 | Email::class, 33 | 'queued_job_admin_email', 34 | Config::inst()->get(Email::class, 'admin_email') 35 | ); 36 | } 37 | 38 | public function createMissingDefaultJobReport(array $jobConfig, string $title): ?Email 39 | { 40 | $subject = sprintf('Default Job "%s" missing', $title); 41 | $from = Config::inst()->get(Email::class, 'queued_job_admin_email'); 42 | $to = array_key_exists('email', $jobConfig ?? []) && $jobConfig['email'] 43 | ? $jobConfig['email'] 44 | : $from; 45 | 46 | if (!$to) { 47 | return null; 48 | } 49 | 50 | return Email::create($from, $to, $subject) 51 | ->setData($jobConfig) 52 | ->addData('Title', $title) 53 | ->addData('Site', Director::absoluteBaseURL()) 54 | ->setHTMLTemplate('QueuedJobsDefaultJob'); 55 | } 56 | 57 | public function createStalledJobReport(string $subject, string $message, int $jobID): ?Email 58 | { 59 | $email = $this->createReport($subject); 60 | if ($email === null) { 61 | return null; 62 | } 63 | 64 | return $email 65 | ->setData([ 66 | 'JobID' => $jobID, 67 | 'Message' => $message, 68 | 'Site' => Director::absoluteBaseURL(), 69 | ]) 70 | ->setHTMLTemplate('QueuedJobsStalledJob'); 71 | } 72 | 73 | public function createBrokenJobsReport(string $subject, string $message, DataList $jobs): ?Email 74 | { 75 | $email = $this->createReport($subject); 76 | if ($email === null) { 77 | return null; 78 | } 79 | return $email 80 | ->setData([ 81 | 'Message' => $message, 82 | 'Jobs' => $jobs, 83 | 'Site' => Director::absoluteBaseURL(), 84 | ]) 85 | ->setHTMLTemplate('QueuedJobsBrokenJobs'); 86 | } 87 | 88 | /** 89 | * Create a generic email report 90 | * useful for reporting queue service issues 91 | */ 92 | public function createReport(string $subject): ?Email 93 | { 94 | $from = Config::inst()->get(Email::class, 'admin_email'); 95 | $to = Config::inst()->get(Email::class, 'queued_job_admin_email'); 96 | 97 | if (!$to) { 98 | return null; 99 | } 100 | 101 | return Email::create($from, $to, $subject); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lang/hr.yml: -------------------------------------------------------------------------------- 1 | hr: 2 | CleanupJob: 3 | Title: 'Očisti stare poslove iz baze' 4 | DeleteObjectJob: 5 | DELETE_JOB: 'Obriši čvor' 6 | DELETE_OBJ2: 'Obriši {title}' 7 | GenerateSitemapJob: 8 | REGENERATE: 'Regeneriraj Google sitemap .xml datoteku' 9 | QueuedJobs: 10 | CREATE_JOB_TYPE: 'Kreiraj posao tipa' 11 | CREATE_NEW_JOB: 'Kreiraj novi posao' 12 | JOB_EXCEPT: 'Posao uzrokovao izuzetak %s u %s na linijama %s' 13 | JOB_PAUSED: 'Posao pauziran u %s' 14 | JOB_STALLED: 'Posao zaustavljen nakon %s pokušaja - molimo provjerite' 15 | JOB_TYPE: 'Tip posla' 16 | JOB_TYPE_PARAMS: 'Konstruktorski parametri za kreiranje posla' 17 | JobsFieldTitle: Poslovi 18 | MEMORY_RELEASE: 'Posao oslobađa memoriju i čeka (%s korišeno)' 19 | STALLED_JOB: 'Zaglavljen posao' 20 | STALLED_JOB_MSG: 'Čini se da posao %s je zaglavljen. Pauziran je, molimo vas logirajte se da provjerite' 21 | START_JOB_TIME: 'Pokreni posao u' 22 | TABLE_ADDE: Dodano 23 | TABLE_MESSAGES: Poruka 24 | TABLE_NUM_PROCESSED: Gotovo 25 | TABLE_STARTED: Započeto 26 | TABLE_START_AFTER: 'Započni nakon' 27 | TABLE_TITLE: Naslov 28 | TABLE_TOTAL: Ukupno 29 | TIME_LIMIT: 'Red je prešao vremenski limit i resetirat će se prije započinjanja' 30 | QueuedJobsAdmin: 31 | MENUTITLE: Poslovi 32 | ScheduledExecution: 33 | EXECUTE_EVERY: 'Izvrši svaki' 34 | EXECUTE_FREE: 'Planirano (u strtotime formatu od prvog pokretanja)' 35 | ExecuteEveryDay: dan 36 | ExecuteEveryFortnight: 'dve nedjelje' 37 | ExecuteEveryHour: sat 38 | ExecuteEveryMinute: minuta 39 | ExecuteEveryMonth: mjesec 40 | ExecuteEveryWeek: tjedan 41 | ExecuteEveryYear: godina 42 | FIRST_EXECUTION: 'Prvo pokretanje' 43 | NEXT_RUN_DATE: 'Sljedeći datum pokretanja' 44 | ScheduleTabTitle: Raspored 45 | ScheduledExecutionJob: 46 | Title: 'Planirano izvršenje za {title}' 47 | Symbiote\QueuedJobs\Controllers\QueuedJobsAdmin: 48 | CREATE_JOB_TYPE: 'Kreiraj posao tipa' 49 | CREATE_NEW_JOB: 'Kreiraj novi posao' 50 | JobsFieldTitle: Poslovi 51 | MENUTITLE: Poslovi 52 | START_JOB_TIME: 'Pokreni posao u' 53 | Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor: 54 | JOB_TYPE: 'Tip posla' 55 | TABLE_ADDE: Dodano 56 | TABLE_MESSAGES: Poruka 57 | TABLE_NUM_PROCESSED: Gotovo 58 | TABLE_STARTED: Započeto 59 | TABLE_START_AFTER: 'Započni nakon' 60 | TABLE_TITLE: Naslov 61 | TABLE_TOTAL: Ukupno 62 | Symbiote\QueuedJobs\Extensions\ScheduledExecutionExtension: 63 | EXECUTE_EVERY: 'Izvrši svaki' 64 | EXECUTE_FREE: 'Planirano (u strtotime formatu od prvog pokretanja)' 65 | ExecuteEveryDay: dan 66 | ExecuteEveryFortnight: 'dve nedjelje' 67 | ExecuteEveryHour: sat 68 | ExecuteEveryMinute: minuta 69 | ExecuteEveryMonth: mjesec 70 | ExecuteEveryWeek: tjedan 71 | ExecuteEveryYear: godina 72 | FIRST_EXECUTION: 'Prvo pokretanje' 73 | NEXT_RUN_DATE: 'Sljedeći datum pokretanja' 74 | ScheduleTabTitle: Raspored 75 | db_ExecuteEvery: 'Izvrši svaki' 76 | Symbiote\QueuedJobs\Jobs\CleanupJob: 77 | Title: 'Očisti stare poslove iz baze' 78 | Symbiote\QueuedJobs\Jobs\DeleteObjectJob: 79 | DELETE_JOB: 'Obriši čvor' 80 | DELETE_OBJ2: 'Obriši {title}' 81 | Symbiote\QueuedJobs\Jobs\GenerateGoogleSitemapJob: 82 | REGENERATE: 'Regeneriraj Google sitemap .xml datoteku' 83 | Symbiote\QueuedJobs\Jobs\ScheduledExecutionJob: 84 | Title: 'Planirano izvršenje za {title}' 85 | Symbiote\QueuedJobs\Services\QueuedJobService: 86 | STALLED_JOB: 'Zaglavljen posao' 87 | TIME_LIMIT: 'Red je prešao vremenski limit i resetirat će se prije započinjanja' 88 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1_bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🪳 Bug Report 2 | description: Tell us if something isn't working the way it's supposed to 3 | 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | We strongly encourage you to [submit a pull request](https://docs.silverstripe.org/en/contributing/code/) which fixes the issue. 9 | Bug reports which are accompanied with a pull request are a lot more likely to be resolved quickly. 10 | - type: input 11 | id: affected-versions 12 | attributes: 13 | label: Module version(s) affected 14 | description: | 15 | What version of _this module_ have you reproduced this bug on? 16 | Run `composer info` to see the specific version of each module installed in your project. 17 | If you don't have access to that, check inside the help menu in the bottom left of the CMS. 18 | placeholder: x.y.z 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: description 23 | attributes: 24 | label: Description 25 | description: A clear and concise description of the problem 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: how-to-reproduce 30 | attributes: 31 | label: How to reproduce 32 | description: | 33 | ⚠️ This is the most important part of the report ⚠️ 34 | Without a way to easily reproduce your issue, there is little chance we will be able to help you and work on a fix. 35 | - Please, take the time to show us some code and/or configuration that is needed for others to reproduce the problem easily. 36 | - If the bug is too complex to reproduce with some short code samples, please reproduce it in a public repository and provide a link to the repository along with steps for setting up and reproducing the bug using that repository. 37 | - If part of the bug includes an error or exception, please provide a full stack trace. 38 | - If any user interaction is required to reproduce the bug, please add an ordered list of steps that are required to reproduce it. 39 | - Be as clear as you can, but don't miss any steps out. Simply saying "create a page" is less useful than guiding us through the steps you're taking to create a page, for example. 40 | placeholder: | 41 | 42 | #### Code sample 43 | ```php 44 | 45 | ``` 46 | 47 | #### Reproduction steps 48 | 1. 49 | validations: 50 | required: true 51 | - type: textarea 52 | id: possible-solution 53 | attributes: 54 | label: Possible Solution 55 | description: | 56 | *Optional: only if you have suggestions on a fix/reason for the bug* 57 | Please consider [submitting a pull request](https://docs.silverstripe.org/en/contributing/code/) with your solution! It helps get faster feedback and greatly increases the chance of the bug being fixed. 58 | - type: textarea 59 | id: additional-context 60 | attributes: 61 | label: Additional Context 62 | description: "*Optional: any other context about the problem: log messages, screenshots, etc.*" 63 | - type: checkboxes 64 | id: validations 65 | attributes: 66 | label: Validations 67 | description: "Before submitting the issue, please make sure you do the following:" 68 | options: 69 | - label: Check that there isn't already an issue that reports the same bug 70 | required: true 71 | - label: Double check that your reproduction steps work in a fresh installation of [`silverstripe/installer`](https://github.com/silverstripe/silverstripe-installer) (with any code examples you've provided) 72 | required: true 73 | -------------------------------------------------------------------------------- /lang/ar.yml: -------------------------------------------------------------------------------- 1 | ar: 2 | CreateQueuedJobTask: 3 | Description: 'مهمة تستخدم لإنشاء وظيفة مجدولة. قم بتمرير اسم تصنيف الوظيفة المجدولة ك "اسم" العامل المتغير, قم بتمرير عامل متغير "بادئ" (قابل للقراءة عن طريق إس تى آر تو تايم) لتحديد وقت بدء الوظيفة.' 4 | DeleteObjectJob: 5 | DELETE_JOB: 'حذف عقدة فى النظام' 6 | DELETE_OBJ2: 'حذف {title}' 7 | GenerateSitemapJob: 8 | REGENERATE: 'إعادة إنتاج ملف جوجل سايت ماب.إكس إم إل' 9 | PublishItemsJob: 10 | Title: 'قم بإعلان العناصر تحت عنوان {title}' 11 | QueuedJobDescriptor: 12 | PLURALNAME: 'الواصفات الوظيفية الموضوعة في قائمة الانتظار' 13 | SINGULARNAME: 'الواصف الوظيفي الموضوع في قائمة الانتظار' 14 | QueuedJobs: 15 | JOB_EXCEPT: 'الوظيفة سببت استثناء s% فى s% فى السطر s%' 16 | JOB_PAUSED: 'تم توقف الوظيفة عند s%' 17 | JOB_STALLED: 'تم تعطيل الوظيفة بعد s% محاولات - من فضلك قم بالتحقق' 18 | JOB_TYPE: 'نوع الوظيفة' 19 | JobsFieldTitle: الوظائف 20 | MEMORY_RELEASE: 'ذاكرة إطلاق الوظائف و الانتظار (s% مستخدم)' 21 | STALLED_JOB: 'الوظيفة المؤجلة' 22 | STALLED_JOB_MSG: 'وظيفة باسم s% تبين أنها تعطلت. قد تم توقفها, من فضلك سجل الدخول لكى تتحقق منها' 23 | TABLE_ADDE: 'تمت إضافته' 24 | TABLE_MESSAGES: رسالة 25 | TABLE_NUM_PROCESSED: تم 26 | TABLE_STARTED: 'تم البدأ' 27 | TABLE_START_AFTER: 'ابدأ بعد' 28 | TABLE_STATUS: الحالة 29 | TABLE_TITLE: عنوان 30 | TABLE_TOTAL: الكلى 31 | QueuedJobsAdmin: 32 | MENUTITLE: وظائف 33 | ScheduledExecution: 34 | EXECUTE_EVERY: 'قم بتنفيذ كل' 35 | EXECUTE_FREE: 'محددة (بصيغة إس تى آر تو تايم من التنفيذ الأول)' 36 | ExecuteEveryDay: يوم 37 | ExecuteEveryFortnight: أسبوعان 38 | ExecuteEveryHour: ساعة 39 | ExecuteEveryMonth: شهر 40 | ExecuteEveryWeek: أسبوع 41 | ExecuteEveryYear: سنة 42 | FIRST_EXECUTION: 'التنفيذ الأول' 43 | NEXT_RUN_DATE: 'موعد التشغيل التالى' 44 | ScheduleTabTitle: 'الجدول الزمني' 45 | ScheduledExecutionJob: 46 | Title: 'موعد التنفيذ المحدد ل {title}' 47 | Symbiote\QueuedJobs\Controllers\QueuedJobsAdmin: 48 | JobsFieldTitle: وظائف 49 | MENUTITLE: وظائف 50 | Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor: 51 | JOB_TYPE: 'نوع الوظيفة' 52 | PLURALNAME: 'الواصفات الوظيفية الموضوعة في قائمة الانتظار' 53 | SINGULARNAME: 'الواصف الوظيفي الموضوع في قائمة الانتظار' 54 | TABLE_ADDE: 'تمت إضافته' 55 | TABLE_FINISHED: انتهى 56 | TABLE_MESSAGES: رسالة 57 | TABLE_NUM_PROCESSED: تم 58 | TABLE_STARTED: 'تم البدأ' 59 | TABLE_START_AFTER: 'ابدأ بعد' 60 | TABLE_STATUS: الحالة 61 | TABLE_TITLE: عنوان 62 | TABLE_TOTAL: المجموع 63 | Symbiote\QueuedJobs\Extensions\ScheduledExecutionExtension: 64 | EXECUTE_EVERY: 'قم بتنفيذ كل' 65 | EXECUTE_FREE: 'محددة (بصيغة إس تى آر تو تايم من التنفيذ الأول)' 66 | ExecuteEveryDay: يوم 67 | ExecuteEveryFortnight: أسبوعان 68 | ExecuteEveryHour: ساعة 69 | ExecuteEveryMonth: شهر 70 | ExecuteEveryWeek: أسبوع 71 | ExecuteEveryYear: سنة 72 | FIRST_EXECUTION: 'التنفيذ الأول' 73 | NEXT_RUN_DATE: 'موعد التشغيل التالى' 74 | ScheduleTabTitle: 'الجدول الزمني' 75 | db_ExecuteEvery: 'قم بتنفيذ كل' 76 | Symbiote\QueuedJobs\Jobs\DeleteObjectJob: 77 | DELETE_JOB: 'حذف عقدة فى النظام' 78 | DELETE_OBJ2: 'حذف {title}' 79 | Symbiote\QueuedJobs\Jobs\GenerateGoogleSitemapJob: 80 | REGENERATE: 'إعادة إنتاج ملف جوجل سايت ماب.إكس إم إل' 81 | Symbiote\QueuedJobs\Jobs\PublishItemsJob: 82 | Title: 'قم بإعلان العناصر تحت عنوان {title}' 83 | Symbiote\QueuedJobs\Jobs\ScheduledExecutionJob: 84 | Title: 'موعد التنفيذ المحدد ل {title}' 85 | Symbiote\QueuedJobs\Services\QueuedJobService: 86 | STALLED_JOB: 'الوظيفة المؤجلة' 87 | Symbiote\QueuedJobs\Tasks\CreateQueuedJobTask: 88 | Description: 'مهمة تستخدم لإنشاء وظيفة مجدولة. قم بتمرير اسم تصنيف الوظيفة المجدولة ك "اسم" العامل المتغير, قم بتمرير عامل متغير "بادئ" (قابل للقراءة عن طريق إس تى آر تو تايم) لتحديد وقت بدء الوظيفة.' 89 | -------------------------------------------------------------------------------- /src/Tasks/ProcessJobQueueTask.php: -------------------------------------------------------------------------------- 1 | 21 | * @license BSD http://silverstripe.org/bsd-license/ 22 | */ 23 | class ProcessJobQueueTask extends BuildTask 24 | { 25 | protected static string $commandName = 'ProcessJobQueueTask'; 26 | 27 | public static function getDescription(): string 28 | { 29 | return _t( 30 | __CLASS__ . '.Description', 31 | 'Used via a cron job to execute queued jobs that need to be run.' 32 | ); 33 | } 34 | 35 | protected function execute(InputInterface $input, PolyOutput $output): int 36 | { 37 | if (QueuedJobService::singleton()->isMaintenanceLockActive()) { 38 | return Command::FAILURE; 39 | } 40 | $queue = AbstractQueuedJob::getQueue($input->getOption('queue')); 41 | if ($queue === null) { 42 | $output->writeln('queue must be one of "immediate", "queued", or "large"'); 43 | return Command::INVALID; 44 | } 45 | 46 | $service = $this->getService(); 47 | 48 | // Ensure that log messages are visible when executing this task in CLI. 49 | // Running the task via browser doesn't need this output because you can check the job in the CMS. 50 | // Note that if we want to output this to the browser in the future, simply removing this condition 51 | // isn't enough, because it'll end up double-logging in the job messages tab. 52 | if (Director::is_cli()) { 53 | $logger = $service->getLogger(); 54 | if ($logger instanceof Logger) { 55 | $logger->pushHandler(PolyOutputLogHandler::create($output)); 56 | } 57 | } 58 | 59 | if ($input->getOption('list')) { 60 | // List helper 61 | $service->queueRunner->listJobs(); 62 | return Command::SUCCESS; 63 | } 64 | 65 | // Check if there is a job to run 66 | $job = $input->getOption('job'); 67 | if ($job && strpos($job, '-')) { 68 | // Run from a single job 69 | $parts = explode('-', $job ?? ''); 70 | $id = $parts[1]; 71 | $service->runJob($id); 72 | return Command::SUCCESS; 73 | } 74 | 75 | // Run the queue 76 | $service->runQueue($queue); 77 | return Command::SUCCESS; 78 | } 79 | 80 | /** 81 | * Returns an instance of the QueuedJobService. 82 | * 83 | * @return QueuedJobService 84 | */ 85 | public function getService() 86 | { 87 | return QueuedJobService::singleton(); 88 | } 89 | 90 | public function getOptions(): array 91 | { 92 | return [ 93 | new InputOption('list', null, InputOption::VALUE_NONE, 'List jobs instead of processing a queue'), 94 | new InputOption('job', null, InputOption::VALUE_REQUIRED, 'A specific job to run'), 95 | new InputOption( 96 | 'queue', 97 | null, 98 | InputOption::VALUE_REQUIRED, 99 | 'The queue to process', 100 | 'queued', 101 | ['immediate', 'queued', 'large'] 102 | ), 103 | ]; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lang/fi.yml: -------------------------------------------------------------------------------- 1 | fi: 2 | CleanupJob: 3 | Title: 'Siivoa vanhat työt tietokannasta' 4 | DeleteObjectJob: 5 | DELETE_JOB: 'Poista solmu' 6 | DELETE_OBJ2: 'Poista {title}' 7 | GenerateSitemapJob: 8 | REGENERATE: 'Luo Google-sivukartta .xml-tiedosto uudelleen' 9 | PublishItemsJob: 10 | Title: 'Julkaise kohteet {title} alla' 11 | QueuedJobDescriptor: 12 | PLURALNAME: 'Jonossa olevien tehtävien kuvaajat' 13 | SINGULARNAME: 'Jonossa olevan tehtävän kuvaaja' 14 | QueuedJobRule: 15 | PLURALNAME: 'Jonossa olevan tehtävän säännöt' 16 | SINGULARNAME: 'Jonossa olevan tehtävän sääntö' 17 | QueuedJobs: 18 | CREATE_JOB_TYPE: 'Luo tehtävä tyypiltään' 19 | CREATE_NEW_JOB: 'Luo uusi tehtävä' 20 | JOB_EXCEPT: 'Tehtävä aiheutti poikkeuksen %s sijainnissa %s rivillä %s' 21 | JOB_PAUSED: 'Tehtävä pysäytetty aikaan %s' 22 | JOB_TYPE: Tehtävätyyppi 23 | JobsFieldTitle: Tehtävät 24 | STALLED_JOB: 'Pysäytetty työ' 25 | STALLED_JOB_MSG: '%s tehtävä näyttää olevan seisahtunut. Se on nyt pysäytetty. Ole hyvä ja kirjaudu sisään tarkastellaksesi sitä' 26 | START_JOB_TIME: 'Aloita tehtävä ajankohtana' 27 | TABLE_ADDE: Lisätty 28 | TABLE_MESSAGES: Viesti 29 | TABLE_NUM_PROCESSED: Tehty 30 | TABLE_STARTED: Aloitettu 31 | TABLE_START_AFTER: 'Aloita jälkeen' 32 | TABLE_STATUS: Tila 33 | TABLE_TITLE: Otsikko 34 | TABLE_TOTAL: Yhteensä 35 | QueuedJobsAdmin: 36 | MENUTITLE: Tehtävät 37 | ScheduledExecution: 38 | EXECUTE_EVERY: 'Suorita joka' 39 | ExecuteEveryDay: Päivä 40 | ExecuteEveryFortnight: 'Kaksi viikkoa' 41 | ExecuteEveryHour: Tunti 42 | ExecuteEveryMinute: Minuutti 43 | ExecuteEveryMonth: Kuukausi 44 | ExecuteEveryWeek: Viikko 45 | ExecuteEveryYear: Vuosi 46 | FIRST_EXECUTION: 'Ensimmäinen suoritus' 47 | NEXT_RUN_DATE: 'Seuraava suorituspäivä' 48 | ScheduleTabTitle: Aikatauluta 49 | ScheduledExecutionJob: 50 | Title: 'Suoritusaikataulu kohteelle {title}' 51 | Symbiote\QueuedJobs\Controllers\QueuedJobsAdmin: 52 | CREATE_JOB_TYPE: 'Luo tehtävä tyypiltään' 53 | CREATE_NEW_JOB: 'Luo uusi tehtävä' 54 | JobsFieldTitle: Tehtävät 55 | MENUTITLE: Tehtävät 56 | START_JOB_TIME: 'Aloita tehtävä ajankohtana' 57 | Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor: 58 | JOB_TYPE: Tehtävätyyppi 59 | PLURALNAME: 'Jonossa olevien tehtävien kuvaajat' 60 | SINGULARNAME: 'Jonossa olevan tehtävän kuvaaja' 61 | TABLE_ADDE: Lisätty 62 | TABLE_MESSAGES: Viesti 63 | TABLE_NUM_PROCESSED: Tehty 64 | TABLE_STARTED: Aloitettu 65 | TABLE_START_AFTER: 'Aloita jälkeen' 66 | TABLE_STATUS: Tila 67 | TABLE_TITLE: Otsikko 68 | TABLE_TOTAL: Yhteensä 69 | Symbiote\QueuedJobs\DataObjects\QueuedJobRule: 70 | PLURALNAME: 'Jonossa olevan tehtävän säännöt' 71 | SINGULARNAME: 'Jonossa olevan tehtävän sääntö' 72 | Symbiote\QueuedJobs\Extensions\ScheduledExecutionExtension: 73 | EXECUTE_EVERY: 'Suorita joka' 74 | ExecuteEveryDay: Päivä 75 | ExecuteEveryFortnight: 'Kaksi viikkoa' 76 | ExecuteEveryHour: Tunti 77 | ExecuteEveryMinute: Minuutti 78 | ExecuteEveryMonth: Kuukausi 79 | ExecuteEveryWeek: Viikko 80 | ExecuteEveryYear: Vuosi 81 | FIRST_EXECUTION: 'Ensimmäinen suoritus' 82 | NEXT_RUN_DATE: 'Seuraava suorituspäivä' 83 | ScheduleTabTitle: Aikatauluta 84 | db_ExecuteEvery: 'Suorita joka' 85 | Symbiote\QueuedJobs\Jobs\CleanupJob: 86 | Title: 'Siivoa vanhat työt tietokannasta' 87 | Symbiote\QueuedJobs\Jobs\DeleteObjectJob: 88 | DELETE_JOB: 'Poista solmu' 89 | DELETE_OBJ2: 'Poista {title}' 90 | Symbiote\QueuedJobs\Jobs\GenerateGoogleSitemapJob: 91 | REGENERATE: 'Luo Google-sivukartta .xml-tiedosto uudelleen' 92 | Symbiote\QueuedJobs\Jobs\PublishItemsJob: 93 | Title: 'Julkaise kohteet {title} alla' 94 | Symbiote\QueuedJobs\Jobs\ScheduledExecutionJob: 95 | Title: 'Suoritusaikataulu kohteelle {title}' 96 | Symbiote\QueuedJobs\Services\QueuedJobService: 97 | STALLED_JOB: 'Pysäytetty työ' 98 | -------------------------------------------------------------------------------- /lang/mi.yml: -------------------------------------------------------------------------------- 1 | mi: 2 | CreateQueuedJobTask: 3 | Description: 'He taumahi e whakamahia ana hei hanga mahi kua tūtirahia. Tukuna te ingoa tauaha mahi kua tūtirahia hei tawhā "ingoa", tukuna he tawhā "tīmata" kōwhiringa (ka taea te poroporo mā te strtotime) hei tautuhi i te wā tīmata mō te mahi.' 4 | DeleteObjectJob: 5 | DELETE_JOB: 'Muku kōpuku' 6 | DELETE_OBJ2: 'Muku {title}' 7 | GenerateSitemapJob: 8 | REGENERATE: 'Whakatupuriā anō te kōnae .xml maherepae Google' 9 | PublishItemsJob: 10 | Title: 'Whakaputa tuemi ki raro i {title}' 11 | QueuedJobDescriptor: 12 | PLURALNAME: 'Ngā Pūwhakaahua Mahi Kua Tūtiratia' 13 | SINGULARNAME: 'Pūwhakaahua Mahi Kua Tūtiratia' 14 | QueuedJobs: 15 | JOB_EXCEPT: 'nā te mahi i whakaputa he aweretanga %s i %s i te rārangi %s' 16 | JOB_PAUSED: 'i okioki te mahi i %s' 17 | JOB_STALLED: 'I auporoa te mahi i muri i ngā whakamātauranga %s - me tirotiro' 18 | JOB_TYPE: 'Momo mahi' 19 | JobsFieldTitle: 'Ngā Mahi' 20 | MEMORY_RELEASE: 'E tuku pūmahara ana te mahi, ka tataru (i whakamahia te %s)' 21 | STALLED_JOB: 'Mahi Kua Auporoa' 22 | STALLED_JOB_MSG: 'Te āhua nei, kua auporoa he mahi e kīia ana ko %s. Kua okioki, me takiuru anō kia tirohia' 23 | TABLE_ADDE: 'I Tāpiritia' 24 | TABLE_MESSAGES: Karere 25 | TABLE_NUM_PROCESSED: 'Kua Oti' 26 | TABLE_STARTED: 'I Tīmata' 27 | TABLE_START_AFTER: 'Tīmata Ā Muri' 28 | TABLE_STATUS: Tūnga 29 | TABLE_TITLE: Taitara 30 | TABLE_TOTAL: Tapeke 31 | QueuedJobsAdmin: 32 | MENUTITLE: 'Ngā Mahi' 33 | ScheduledExecution: 34 | EXECUTE_EVERY: 'Kawea Te Katoa' 35 | EXECUTE_FREE: 'Kua whakaritea ( i te hōputu strtotime mai i te kawenga tuatahi)' 36 | ExecuteEveryDay: Rā 37 | ExecuteEveryFortnight: 'Rua Wiki' 38 | ExecuteEveryHour: Haora 39 | ExecuteEveryMonth: Marama 40 | ExecuteEveryWeek: Wiki 41 | ExecuteEveryYear: Tau 42 | FIRST_EXECUTION: 'Kawenga Tuatahi' 43 | NEXT_RUN_DATE: 'Rā whakahaere anō' 44 | ScheduledExecutionJob: 45 | Title: 'Ka whakaritea te kawenga mō {title}' 46 | Symbiote\QueuedJobs\Controllers\QueuedJobsAdmin: 47 | JobsFieldTitle: 'Ngā Mahi' 48 | MENUTITLE: 'Ngā Mahi' 49 | Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor: 50 | JOB_TYPE: 'Momo mahi' 51 | PLURALNAME: 'Ngā Pūwhakaahua Mahi Kua Tūtiratia' 52 | SINGULARNAME: 'Pūwhakaahua Mahi Kua Tūtiratia' 53 | TABLE_ADDE: 'I Tāpiritia' 54 | TABLE_FINISHED: 'Kua Oti' 55 | TABLE_MESSAGES: Karere 56 | TABLE_NUM_PROCESSED: 'Kua Oti' 57 | TABLE_STARTED: 'I Tīmata' 58 | TABLE_START_AFTER: 'Tīmata Ā Muri' 59 | TABLE_STATUS: Tūnga 60 | TABLE_TITLE: Taitara 61 | TABLE_TOTAL: Tapeke 62 | Symbiote\QueuedJobs\Extensions\ScheduledExecutionExtension: 63 | EXECUTE_EVERY: 'Kawea Te Katoa' 64 | EXECUTE_FREE: 'Kua whakaritea ( i te hōputu strtotime mai i te kawenga tuatahi)' 65 | ExecuteEveryDay: Rā 66 | ExecuteEveryFortnight: 'Rua Wiki' 67 | ExecuteEveryHour: Haora 68 | ExecuteEveryMonth: Marama 69 | ExecuteEveryWeek: Wiki 70 | ExecuteEveryYear: Tau 71 | FIRST_EXECUTION: 'Kawenga Tuatahi' 72 | NEXT_RUN_DATE: 'Rā whakahaere anō' 73 | db_ExecuteEvery: 'Kawea Te Katoa' 74 | Symbiote\QueuedJobs\Jobs\DeleteObjectJob: 75 | DELETE_JOB: 'Muku kōpuku' 76 | DELETE_OBJ2: 'Muku {title}' 77 | Symbiote\QueuedJobs\Jobs\GenerateGoogleSitemapJob: 78 | REGENERATE: 'Whakatupuriā anō te kōnae .xml maherepae Google' 79 | Symbiote\QueuedJobs\Jobs\PublishItemsJob: 80 | Title: 'Whakaputa tuemi ki raro i {title}' 81 | Symbiote\QueuedJobs\Jobs\ScheduledExecutionJob: 82 | Title: 'Ka whakaritea te kawenga mō {title}' 83 | Symbiote\QueuedJobs\Services\QueuedJobService: 84 | STALLED_JOB: 'Mahi Kua Auporoa' 85 | Symbiote\QueuedJobs\Tasks\CreateQueuedJobTask: 86 | Description: 'He taumahi e whakamahia ana hei hanga mahi kua tūtirahia. Tukuna te ingoa tauaha mahi kua tūtirahia hei tawhā "ingoa", tukuna he tawhā "tīmata" kōwhiringa (ka taea te poroporo mā te strtotime) hei tautuhi i te wā tīmata mō te mahi.' 87 | -------------------------------------------------------------------------------- /src/Tasks/CreateQueuedJobTask.php: -------------------------------------------------------------------------------- 1 | 26 | * @license BSD http://silverstripe.org/bsd-license/ 27 | */ 28 | class CreateQueuedJobTask extends BuildTask 29 | { 30 | protected static string $commandName = 'CreateQueuedJobTask'; 31 | 32 | public static function getDescription(): string 33 | { 34 | return _t( 35 | __CLASS__ . '.Description', 36 | 'A task used to create a queued job. Pass the queued job class name as the "name" parameter, ' 37 | . 'pass an optional "start" parameter (parseable by strtotime) to set a start time for the job.' 38 | ); 39 | } 40 | 41 | protected function execute(InputInterface $input, PolyOutput $output): int 42 | { 43 | $name = $input->getOption('name'); 44 | if ($name && ClassInfo::exists($name)) { 45 | $clz = $name; 46 | $job = new $clz(); 47 | } else { 48 | $job = new DummyQueuedJob(mt_rand(10, 100)); 49 | } 50 | 51 | $start = $input->getOption('start'); 52 | if ($start) { 53 | $start = strtotime($start); 54 | $now = DBDatetime::now()->getTimestamp(); 55 | if ($start >= $now) { 56 | $friendlyStart = DBDatetime::create()->setValue($start)->Rfc2822(); 57 | $output->writeln('Job queued to start at: ' . $friendlyStart . ''); 58 | QueuedJobService::singleton()->queueJob($job, $start); 59 | } else { 60 | $output->writeln("'start' parameter must be a date/time in the future, parseable with strtotime"); 61 | } 62 | } else { 63 | $output->writeln('Job Queued'); 64 | QueuedJobService::singleton()->queueJob($job); 65 | } 66 | return Command::SUCCESS; 67 | } 68 | 69 | public function getOptions(): array 70 | { 71 | return [ 72 | new InputOption( 73 | 'name', 74 | null, 75 | InputOption::VALUE_REQUIRED, 76 | 'Fully qualified classname for the job to queue', 77 | suggestedValues: Closure::fromCallable([static::class, 'getAllQueuedJobClasses']) 78 | ), 79 | new InputOption( 80 | 'start', 81 | null, 82 | InputOption::VALUE_REQUIRED, 83 | 'When to start the job. Must be parsable by ' 84 | . 'strtotime' 85 | ), 86 | ]; 87 | } 88 | 89 | public static function getAllQueuedJobClasses(): array 90 | { 91 | $implementors = ClassInfo::implementorsOf(QueuedJob::class); 92 | $classes = []; 93 | foreach ($implementors as $class) { 94 | $subclasses = ClassInfo::subclassesFor($class); 95 | foreach ($subclasses as $subclass) { 96 | $reflectionClass = new ReflectionClass($subclass); 97 | if ($reflectionClass->isAbstract()) { 98 | continue; 99 | } 100 | $classes[] = $subclass; 101 | } 102 | } 103 | return $classes; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Services/QueuedJob.php: -------------------------------------------------------------------------------- 1 | 9 | * @license BSD http://silverstripe.org/bsd-license/ 10 | */ 11 | interface QueuedJob 12 | { 13 | /** 14 | * Always run immediate jobs as soon as possible 15 | * @var string 16 | */ 17 | public const IMMEDIATE = '1'; 18 | 19 | /** 20 | * Queued jobs may have some processing to do, but should be pretty quick 21 | * @var string 22 | */ 23 | public const QUEUED = '2'; 24 | 25 | /** 26 | * Large jobs will take minutes, not seconds to run 27 | * @var string 28 | */ 29 | public const LARGE = '3'; 30 | 31 | /** 32 | * Statuses 33 | * @var string 34 | */ 35 | public const STATUS_NEW = 'New'; 36 | public const STATUS_INIT = 'Initialising'; 37 | public const STATUS_RUN = 'Running'; 38 | public const STATUS_WAIT = 'Waiting'; 39 | public const STATUS_COMPLETE = 'Complete'; 40 | public const STATUS_PAUSED = 'Paused'; 41 | public const STATUS_CANCELLED = 'Cancelled'; 42 | public const STATUS_BROKEN = 'Broken'; 43 | 44 | /** 45 | * Gets a title for the job that can be used in listings 46 | * 47 | * @return string 48 | */ 49 | public function getTitle(); 50 | 51 | /** 52 | * Gets a unique signature for this job and its current parameters. 53 | * 54 | * This is used so that a job isn't added to a queue multiple times - this for example, an indexing job 55 | * might be added every time an item is saved, but it isn't processed immediately. We dont NEED to do the indexing 56 | * more than once (ie the first indexing will still catch any subsequent changes), so we don't need to have 57 | * it in the queue more than once. 58 | * 59 | * If you have a job that absolutely must run multiple times, the AbstractQueuedJob class provides a time sensitive 60 | * randomSignature() method that can be used for returning a random signature each time 61 | * 62 | * @return string 63 | */ 64 | public function getSignature(); 65 | 66 | /** 67 | * Setup this queued job. This is only called the first time this job is executed 68 | * (ie when currentStep is 0) 69 | */ 70 | public function setup(); 71 | 72 | /** 73 | * Called whenever a job is restarted for whatever reason. 74 | * 75 | * This is a separate method so that broken jobs can do some fixup before restarting. 76 | */ 77 | public function prepareForRestart(); 78 | 79 | /** 80 | * What type of job is this? Options are 81 | * - QueuedJob::IMMEDIATE 82 | * - QueuedJob::QUEUED 83 | * - QueuedJob::LARGE 84 | */ 85 | public function getJobType(); 86 | 87 | /** 88 | * A job is run within an external processing loop that will call this method while there are still steps left 89 | * to complete in the job. 90 | * 91 | * Typically, this method should process just a small amount of data - after calling this method, the process 92 | * loop will save the current state of the job to protect against potential failures or errors. 93 | */ 94 | public function process(); 95 | 96 | /** 97 | * Returns true or false to indicate that this job is finished 98 | */ 99 | public function jobFinished(); 100 | 101 | /** 102 | * Return the current job state as an object containing data 103 | * 104 | * stdClass ( 105 | * 'totalSteps' => the total number of steps in this job - this is relayed to the user as an indicator of time 106 | * 'currentStep' => the current number of steps done so far. 107 | * 'isComplete' => whether the job is finished yet 108 | * 'jobData' => data that the job wants persisted when it is stopped or started 109 | * 'messages' => a cumulative array of messages that have occurred during this job so far 110 | * ) 111 | */ 112 | public function getJobData(); 113 | 114 | /** 115 | * Sets data about the job 116 | * 117 | * is an inverse of the getJobData() method, but being explicit about what data is set 118 | * 119 | * @param int $totalSteps 120 | * @param int $currentStep 121 | * @param boolean $isComplete 122 | * @param \stdClass $jobData 123 | * @param array $messages 124 | * 125 | * @see QueuedJob::getJobData(); 126 | */ 127 | public function setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages); 128 | 129 | /** 130 | * Add an arbitrary text message into a job 131 | * 132 | * @param string $message 133 | */ 134 | public function addMessage($message); 135 | } 136 | -------------------------------------------------------------------------------- /src/DataObjects/QueuedJobRule.php: -------------------------------------------------------------------------------- 1 | 'Int', 33 | 'Handler' => 'Varchar', 34 | 'MinimumProcessorUsage' => 'Decimal', 35 | 'MaximumProcessorUsage' => 'Decimal', 36 | 'MinimumMemoryUsage' => 'Decimal', 37 | 'MaximumMemoryUsage' => 'Decimal', 38 | 'MinimumSiblingProcessorUsage' => 'Decimal', 39 | 'MaximumSiblingProcessorUsage' => 'Decimal', 40 | 'MinimumSiblingMemoryUsage' => 'Decimal', 41 | 'MaximumSiblingMemoryUsage' => 'Decimal', 42 | ); 43 | 44 | /** 45 | * @inheritdoc 46 | * 47 | * @return int 48 | */ 49 | public function getProcesses() 50 | { 51 | if ($this->getField('Processes') !== null) { 52 | return $this->getField('Processes'); 53 | } 54 | 55 | return 1; 56 | } 57 | 58 | /** 59 | * @inheritdoc 60 | * 61 | * @return null|string 62 | */ 63 | public function getHandler() 64 | { 65 | if ($this->getField('Handler')) { 66 | return $this->getField('Handler'); 67 | } 68 | 69 | return null; 70 | } 71 | 72 | /** 73 | * @return null|float 74 | */ 75 | public function getMinimumProcessorUsage() 76 | { 77 | if ($this->getField('MinimumProcessorUsage') !== null) { 78 | return $this->getField('MinimumProcessorUsage'); 79 | } 80 | 81 | return null; 82 | } 83 | 84 | /** 85 | * @inheritdoc 86 | * 87 | * @return null|float 88 | */ 89 | public function getMaximumProcessorUsage() 90 | { 91 | if ($this->getField('MaximumProcessorUsage') !== null) { 92 | return $this->getField('MaximumProcessorUsage'); 93 | } 94 | 95 | return null; 96 | } 97 | 98 | /** 99 | * @inheritdoc 100 | * 101 | * @return null|float 102 | */ 103 | public function getMinimumMemoryUsage() 104 | { 105 | if ($this->getField('MinimumMemoryUsage') !== null) { 106 | return $this->getField('MinimumMemoryUsage'); 107 | } 108 | 109 | return null; 110 | } 111 | 112 | /** 113 | * @return null|float 114 | */ 115 | public function getMaximumMemoryUsage() 116 | { 117 | if ($this->getField('MaximumMemoryUsage') !== null) { 118 | return $this->getField('MaximumMemoryUsage'); 119 | } 120 | 121 | return null; 122 | } 123 | 124 | /** 125 | * @inheritdoc 126 | * 127 | * @return null|float 128 | */ 129 | public function getMinimumSiblingProcessorUsage() 130 | { 131 | if ($this->getField('MinimumSiblingProcessorUsage') !== null) { 132 | return $this->getField('MinimumSiblingProcessorUsage'); 133 | } 134 | 135 | return null; 136 | } 137 | 138 | /** 139 | * @inheritdoc 140 | * 141 | * @return null|float 142 | */ 143 | public function getMaximumSiblingProcessorUsage() 144 | { 145 | if ($this->getField('MaximumSiblingProcessorUsage') !== null) { 146 | return $this->getField('MaximumSiblingProcessorUsage'); 147 | } 148 | 149 | return null; 150 | } 151 | 152 | /** 153 | * @inheritdoc 154 | * 155 | * @return null|float 156 | */ 157 | public function getMinimumSiblingMemoryUsage() 158 | { 159 | if ($this->getField('MinimumSiblingMemoryUsage') !== null) { 160 | return $this->getField('MinimumSiblingMemoryUsage'); 161 | } 162 | 163 | return null; 164 | } 165 | 166 | /** 167 | * @inheritdoc 168 | * 169 | * @return null|float 170 | */ 171 | public function getMaximumSiblingMemoryUsage() 172 | { 173 | if ($this->getField('MaximumSiblingMemoryUsage') !== null) { 174 | return $this->getField('MaximumSiblingMemoryUsage'); 175 | } 176 | 177 | return null; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Jobs/PublishItemsJob.php: -------------------------------------------------------------------------------- 1 | 16 | * @license BSD http://silverstripe.org/bsd-license/ 17 | */ 18 | class PublishItemsJob extends AbstractQueuedJob implements QueuedJob 19 | { 20 | /** 21 | * @param DataObject $rootNodeID 22 | */ 23 | public function __construct($rootNodeID = null) 24 | { 25 | // this value is automatically persisted between processing requests for 26 | // this job 27 | if ($rootNodeID) { 28 | $this->rootID = $rootNodeID; 29 | } 30 | } 31 | 32 | protected function getRoot() 33 | { 34 | return Page::get()->setUseCache(true)->byID($this->rootID); 35 | } 36 | 37 | /** 38 | * Defines the title of the job 39 | * 40 | * @return string 41 | */ 42 | public function getTitle() 43 | { 44 | $title = 'Unknown'; 45 | 46 | if ($root = $this->getRoot()) { 47 | $title = $root->Title; 48 | } 49 | 50 | return _t( 51 | __CLASS__ . '.Title', 52 | "Publish items beneath {title}", 53 | ['title' => $title] 54 | ); 55 | } 56 | 57 | /** 58 | * Indicate to the system which queue we think we should be in based 59 | * on how many objects we're going to touch on while processing. 60 | * 61 | * We want to make sure we also set how many steps we think we might need to take to 62 | * process everything - note that this does not need to be 100% accurate, but it's nice 63 | * to give a reasonable approximation 64 | * 65 | * @return int 66 | */ 67 | public function getJobType() 68 | { 69 | $this->totalSteps = 'Lots'; 70 | return QueuedJob::QUEUED; 71 | } 72 | 73 | /** 74 | * This is called immediately before a job begins - it gives you a chance 75 | * to initialise job data and make sure everything's good to go 76 | * 77 | * What we're doing in our case is to queue up the list of items we know we need to 78 | * process still (it's not everything - just the ones we know at the moment) 79 | * 80 | * When we go through, we'll constantly add and remove from this queue, meaning 81 | * we never overload it with content 82 | */ 83 | public function setup() 84 | { 85 | if (!$this->getRoot()) { 86 | // we're missing for some reason! 87 | $this->isComplete = true; 88 | $this->remainingChildren = array(); 89 | return; 90 | } 91 | $remainingChildren = array(); 92 | $remainingChildren[] = $this->getRoot()->ID; 93 | $this->remainingChildren = $remainingChildren; 94 | 95 | // we reset this to 1; this is because we only know for sure about 1 item remaining 96 | // as time goes by, this will increase as we discover more items that need processing 97 | $this->totalSteps = 1; 98 | } 99 | 100 | /** 101 | * Lets process a single node, and publish it if necessary 102 | */ 103 | public function process() 104 | { 105 | $remainingChildren = $this->remainingChildren; 106 | 107 | // if there's no more, we're done! 108 | if (!count($remainingChildren ?? [])) { 109 | $this->isComplete = true; 110 | return; 111 | } 112 | 113 | // we need to always increment! This is important, because if we don't then our container 114 | // that executes around us thinks that the job has died, and will stop it running. 115 | $this->currentStep++; 116 | 117 | // lets process our first item - note that we take it off the list of things left to do 118 | $ID = array_shift($remainingChildren); 119 | 120 | // get the page 121 | $page = Page::get()->setUseCache(true)->byID($ID); 122 | if ($page) { 123 | // publish it 124 | $page->publishRecursive(); 125 | 126 | // and add its children to the list to be published 127 | foreach ($page->Children() as $child) { 128 | $remainingChildren[] = $child->ID; 129 | // we increase how many steps we need to do - this means our total steps constantly rises, 130 | // but it gives users an idea of exactly how many more we know about 131 | $this->totalSteps++; 132 | } 133 | $page->destroy(); 134 | unset($page); 135 | } 136 | 137 | // and now we store the new list of remaining children 138 | $this->remainingChildren = $remainingChildren; 139 | 140 | if (!count($remainingChildren ?? [])) { 141 | $this->isComplete = true; 142 | return; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /lang/sl.yml: -------------------------------------------------------------------------------- 1 | sl: 2 | CleanupJob: 3 | Title: 'Odstrani zastarele naloge iz baze' 4 | CreateQueuedJobTask: 5 | Description: 'Procedura za ustvarjanje novih naročenih nalog. Vnesite naziv naročene naloge v "name" in opcijsko še termin za zagon naloge v "start" (strtotime).' 6 | DeleteObjectJob: 7 | DELETE_JOB: 'Izbriši nalogo' 8 | DELETE_OBJ2: 'Izbriši {title}' 9 | GenerateSitemapJob: 10 | REGENERATE: 'Ponovno zgeneriraj sitemap.xml za Google ' 11 | ProcessJobQueueTask: 12 | Description: "Uporablja 'cron job' za izvedbo naročenih nalog, ki morajo biti izvedene." 13 | PublishItemsJob: 14 | Title: 'Objavi elemente pod {title}' 15 | QueuedJobDescriptor: 16 | PLURALNAME: 'Definicije naročenih nalog' 17 | SINGULARNAME: 'Definicija naročene naloge' 18 | QueuedJobRule: 19 | PLURALNAME: 'Pravila naročenih nalog' 20 | SINGULARNAME: 'Pravilo naročene naloge' 21 | QueuedJobs: 22 | CREATE_JOB_TYPE: 'Ustvari nalogo tipa' 23 | CREATE_NEW_JOB: 'Ustvari novo nalogo' 24 | JOB_EXCEPT: 'Naloga je naletela na izjemo %s v %s v vrstici %s' 25 | JOB_PAUSED: 'Naloga je bila ustavljena na %s' 26 | JOB_STALLED: 'Naloga je zastala po %s poskusih - prosimo, preverite' 27 | JOB_TYPE: 'Tip naloge' 28 | JOB_TYPE_PARAMS: 'Parametri za vzpostavitev naloge' 29 | JobsFieldTitle: Naloge 30 | MEMORY_RELEASE: 'Čakam, da naloga sprosti spomin (%s uporabljenega)' 31 | STALLED_JOB: 'Zastale naloge' 32 | STALLED_JOB_MSG: 'Naročena naloga %s je zastala, zato smo ustavili njeno izvajanje. Preverite stanje.' 33 | START_JOB_TIME: 'Izvedi nalogo ob' 34 | TABLE_ADDE: Dodano 35 | TABLE_MESSAGES: Sporočila 36 | TABLE_NUM_PROCESSED: Zaključeno 37 | TABLE_STARTED: Sproženo 38 | TABLE_START_AFTER: 'Izvedi nalogo po' 39 | TABLE_TITLE: Naziv 40 | TABLE_TOTAL: Skupaj 41 | TIME_LIMIT: 'Naročena naloga je presegla časovno omejite, zato se mora ponovno zagnati.' 42 | QueuedJobsAdmin: 43 | MENUTITLE: Naloge 44 | RunBuildTaskJob: 45 | JOB_TITLE: 'Ustvari nalogo za {task}' 46 | ScheduledExecution: 47 | EXECUTE_EVERY: 'Izvedi vsakih' 48 | EXECUTE_FREE: 'Naročeno za (v formatu strtotime od prve izvedbe)' 49 | ExecuteEveryDay: dni 50 | ExecuteEveryFortnight: 'dva tedna' 51 | ExecuteEveryHour: ur 52 | ExecuteEveryMinute: minut 53 | ExecuteEveryMonth: mesecev 54 | ExecuteEveryWeek: tednov 55 | ExecuteEveryYear: let 56 | FIRST_EXECUTION: 'Izvedeno prvič' 57 | NEXT_RUN_DATE: 'Datum naslednje izvedbe' 58 | ScheduleTabTitle: 'Urnik izvajanja' 59 | ScheduledExecutionJob: 60 | Title: 'Naročena izvedba za {title}' 61 | Symbiote\QueuedJobs\Controllers\QueuedJobsAdmin: 62 | CREATE_JOB_TYPE: 'Ustvari nalogo tipa' 63 | CREATE_NEW_JOB: 'Ustvari novo nalogo' 64 | JobsFieldTitle: Naloge 65 | MENUTITLE: Naloge 66 | START_JOB_TIME: 'Izvedi nalogo ob' 67 | Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor: 68 | JOB_TYPE: 'Tip naloge' 69 | PLURALNAME: 'Definicije naročenih nalog' 70 | SINGULARNAME: 'Definicija naročene naloge' 71 | TABLE_ADDE: Dodano 72 | TABLE_MESSAGES: Sporočila 73 | TABLE_NUM_PROCESSED: Zaključeno 74 | TABLE_STARTED: Sproženo 75 | TABLE_START_AFTER: 'Izvedi nalogo po' 76 | TABLE_TITLE: Naziv 77 | TABLE_TOTAL: Skupaj 78 | Symbiote\QueuedJobs\DataObjects\QueuedJobRule: 79 | PLURALNAME: 'Pravila naročenih nalog' 80 | SINGULARNAME: 'Pravilo naročene naloge' 81 | Symbiote\QueuedJobs\Extensions\ScheduledExecutionExtension: 82 | EXECUTE_EVERY: 'Izvedi vsakih' 83 | EXECUTE_FREE: 'Naročeno za (v formatu strtotime od prve izvedbe)' 84 | ExecuteEveryDay: dni 85 | ExecuteEveryFortnight: 'dva tedna' 86 | ExecuteEveryHour: ur 87 | ExecuteEveryMinute: minut 88 | ExecuteEveryMonth: mesecev 89 | ExecuteEveryWeek: tednov 90 | ExecuteEveryYear: let 91 | FIRST_EXECUTION: 'Izvedeno prvič' 92 | NEXT_RUN_DATE: 'Datum naslednje izvedbe' 93 | ScheduleTabTitle: 'Urnik izvajanja' 94 | db_ExecuteEvery: 'Izvedi vsakih' 95 | Symbiote\QueuedJobs\Jobs\CleanupJob: 96 | Title: 'Odstrani zastarele naloge iz baze' 97 | Symbiote\QueuedJobs\Jobs\DeleteObjectJob: 98 | DELETE_JOB: 'Izbriši nalogo' 99 | DELETE_OBJ2: 'Izbriši {title}' 100 | Symbiote\QueuedJobs\Jobs\GenerateGoogleSitemapJob: 101 | REGENERATE: 'Ponovno zgeneriraj sitemap.xml za Google ' 102 | Symbiote\QueuedJobs\Jobs\PublishItemsJob: 103 | Title: 'Objavi elemente pod {title}' 104 | Symbiote\QueuedJobs\Jobs\ScheduledExecutionJob: 105 | Title: 'Naročena izvedba za {title}' 106 | Symbiote\QueuedJobs\Services\QueuedJobService: 107 | STALLED_JOB: 'Zastale naloge' 108 | TIME_LIMIT: 'Naročena naloga je presegla časovno omejite, zato se mora ponovno zagnati.' 109 | Symbiote\QueuedJobs\Tasks\CreateQueuedJobTask: 110 | Description: 'Procedura za ustvarjanje novih naročenih nalog. Vnesite naziv naročene naloge v "name" in opcijsko še termin za zagon naloge v "start" (strtotime).' 111 | Symbiote\QueuedJobs\Tasks\ProcessJobQueueTask: 112 | Description: "Uporablja 'cron job' za izvedbo naročenih nalog, ki morajo biti izvedene." 113 | -------------------------------------------------------------------------------- /lang/sv.yml: -------------------------------------------------------------------------------- 1 | sv: 2 | CleanupJob: 3 | Title: 'Rensa upp gamla jobb från databasen' 4 | DeleteObjectJob: 5 | DELETE_JOB: 'Radera nod' 6 | DELETE_OBJ2: 'Radera {title}' 7 | GenerateSitemapJob: 8 | REGENERATE: 'Återskapa .xml-filen för Google sitemap' 9 | ProcessJobQueueTask: 10 | Description: 'Används via ett cron-jobb för att utföra köade jobb som måste köras.' 11 | PublishItemsJob: 12 | Title: 'Publicera objekt under {title}' 13 | QueuedJobRule: 14 | PLURALNAME: 'Uppköat jobb regler' 15 | QueuedJobs: 16 | CREATE_JOB_TYPE: 'Skapa jobb av typ' 17 | CREATE_NEW_JOB: 'Skapa nytt jobb' 18 | JOB_EXCEPT: 'Jobbet orsakade erroret %s i %s vid linje %s' 19 | JOB_PAUSED: 'Jobb pausat vid %s' 20 | JOB_STALLED: 'Jobbet stannade efter %s försök, vänligen kontrollera' 21 | JOB_TYPE: Jobbtyp 22 | JOB_TYPE_PARAMS: 'Konstruktörsparametrar för att skapa jobb' 23 | JobsFieldTitle: Jobb 24 | MEMORY_RELEASE: 'Jobbet frigör minne och väntar (%s används)' 25 | STALLED_JOB: 'Stannat jobb' 26 | START_JOB_TIME: 'Börja jobbet vid' 27 | TABLE_ADDE: 'Lagt till' 28 | TABLE_MESSAGES: Meddelande 29 | TABLE_NUM_PROCESSED: Färdig 30 | TABLE_STARTED: Startad 31 | TABLE_START_AFTER: 'Börja efter' 32 | TABLE_TITLE: Titel 33 | TABLE_TOTAL: Totalt 34 | TIME_LIMIT: 'Kön har passerat tidsgränsen och kommer att starta om innan den fortsätter' 35 | QueuedJobsAdmin: 36 | MENUTITLE: Jobb 37 | RunBuildTaskJob: 38 | JOB_TITLE: 'Kör BuildTask {task}' 39 | ScheduledExecution: 40 | EXECUTE_EVERY: 'Kör varje' 41 | EXECUTE_FREE: 'Schemalagt (i strtotime-format från första körning)' 42 | ExecuteEveryDay: Dag 43 | ExecuteEveryFortnight: 'Två veckor' 44 | ExecuteEveryHour: Timme 45 | ExecuteEveryMinute: Minut 46 | ExecuteEveryMonth: Månad 47 | ExecuteEveryWeek: Vecka 48 | ExecuteEveryYear: År 49 | FIRST_EXECUTION: 'Första exekvering' 50 | NEXT_RUN_DATE: 'Nästa körningsdatum' 51 | ScheduleTabTitle: Schema 52 | ScheduledExecutionJob: 53 | Title: 'Schemalagd körning för {title}' 54 | Symbiote\QueuedJobs\Controllers\QueuedJobsAdmin: 55 | CREATE_JOB_TYPE: 'Skapa jobb av typ' 56 | CREATE_NEW_JOB: 'Skapa nytt jobb' 57 | JobsFieldTitle: Jobb 58 | MENUTITLE: Jobb 59 | START_JOB_TIME: 'Börja jobbet vid' 60 | Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor: 61 | JOB_TYPE: Jobbtyp 62 | TABLE_ADDE: 'Lagt till' 63 | TABLE_FINISHED: Slutförd 64 | TABLE_MESSAGES: Meddelande 65 | TABLE_NUM_PROCESSED: Färdig 66 | TABLE_STARTED: Startad 67 | TABLE_START_AFTER: 'Börja efter' 68 | TABLE_TITLE: Titel 69 | TABLE_TOTAL: Totalt 70 | TYPE_IMMEDIATE: Direkt 71 | TYPE_LARGE: Stor 72 | TYPE_QUEUED: 'I kö' 73 | db_Expiry: Upphör 74 | db_JobFinished: 'Jobbet avslutat' 75 | db_JobRestarted: 'Jobbet startade om' 76 | db_JobStarted: 'Jobb börjat' 77 | db_JobStatus: Jobbstatus 78 | db_JobTitle: 'Jobb titel' 79 | db_JobType: Jobbtyp 80 | db_LastProcessedCount: 'Antal senast behandlade' 81 | db_NotifiedBroken: 'Anmäld sönder' 82 | db_ResumeCounts: 'Antal gånger återupptagen' 83 | db_SavedJobData: 'Sparad jobbdata' 84 | db_SavedJobMessages: 'Sparade jobbmeddelanden' 85 | db_Signature: Signatur 86 | db_StartAfter: 'Börja efter' 87 | db_StepsProcessed: 'Steg bearbetade' 88 | db_TotalSteps: 'Totalt antal steg' 89 | db_Worker: Arbetare 90 | db_WorkerCount: 'Antal arbetare' 91 | has_one_RunAs: 'Kör som' 92 | Symbiote\QueuedJobs\DataObjects\QueuedJobRule: 93 | PLURALNAME: 'Uppköat jobb regler' 94 | db_Processes: Processer 95 | Symbiote\QueuedJobs\Extensions\ScheduledExecutionExtension: 96 | EXECUTE_EVERY: 'Kör varje' 97 | EXECUTE_FREE: 'Schemalagt (i strtotime-format från första körning)' 98 | ExecuteEveryDay: Dag 99 | ExecuteEveryFortnight: 'Två veckor' 100 | ExecuteEveryHour: Timme 101 | ExecuteEveryMinute: Minut 102 | ExecuteEveryMonth: Månad 103 | ExecuteEveryWeek: Vecka 104 | ExecuteEveryYear: År 105 | FIRST_EXECUTION: 'Första exekvering' 106 | NEXT_RUN_DATE: 'Nästa körningsdatum' 107 | ScheduleTabTitle: Schema 108 | db_ExecuteEvery: 'Kör varje' 109 | has_one_ScheduledJob: 'Schemalagda jobb' 110 | Symbiote\QueuedJobs\Jobs\CleanupJob: 111 | Title: 'Rensa upp gamla jobb från databasen' 112 | Symbiote\QueuedJobs\Jobs\DeleteObjectJob: 113 | DELETE_JOB: 'Radera nod' 114 | DELETE_OBJ2: 'Radera {title}' 115 | Symbiote\QueuedJobs\Jobs\GenerateGoogleSitemapJob: 116 | REGENERATE: 'Återskapa .xml-filen för Google sitemap' 117 | Symbiote\QueuedJobs\Jobs\PublishItemsJob: 118 | Title: 'Publicera objekt under {title}' 119 | Symbiote\QueuedJobs\Jobs\ScheduledExecutionJob: 120 | Title: 'Schemalagd körning för {title}' 121 | Symbiote\QueuedJobs\Services\QueuedJobService: 122 | STALLED_JOB: 'Stannat jobb' 123 | TIME_LIMIT: 'Kön har passerat tidsgränsen och kommer att starta om innan den fortsätter' 124 | Symbiote\QueuedJobs\Tasks\ProcessJobQueueTask: 125 | Description: 'Används via ett cron-jobb för att utföra köade jobb som måste köras.' 126 | -------------------------------------------------------------------------------- /lang/ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | CleanupJob: 3 | Title: 'Стереть старые задания из базы данных' 4 | CreateQueuedJobTask: 5 | Description: 'Задача используется для создания отложенного действия. Укажите тип задачи, укажите опциональный параметр "start" (обрабатывается функцией strtotime) для времени начала.' 6 | DeleteObjectJob: 7 | DELETE_JOB: Удалить 8 | DELETE_OBJ2: 'Удалить {title}' 9 | GenerateSitemapJob: 10 | REGENERATE: 'Генерировать Google sitemap .xml файл' 11 | ProcessJobQueueTask: 12 | Description: 'Используется cron для выполнения отложенных задач.' 13 | PublishItemsJob: 14 | Title: 'Опубликовать вложенные объекты {title}' 15 | QueuedJobDescriptor: 16 | PLURALNAME: 'Дескрипторы отложенных задач' 17 | SINGULARNAME: 'Дескриптор отложенной задачи' 18 | QueuedJobRule: 19 | PLURALNAME: 'Настройки отложенных задач' 20 | SINGULARNAME: 'Настройки отложенной задачи' 21 | QueuedJobs: 22 | CREATE_JOB_TYPE: 'Создать задачу типа' 23 | CREATE_NEW_JOB: 'Создать задачу' 24 | JOB_EXCEPT: 'Произошла ошибка задачи %s в %s на строчке %s' 25 | JOB_PAUSED: 'Задача остановлена %s' 26 | JOB_STALLED: 'Задача замороженна после %s попыток - пожалуйста проверьте' 27 | JOB_TYPE: 'Тип задачи' 28 | JOB_TYPE_PARAMS: 'Параметры конструктора задачи' 29 | JobsFieldTitle: Задачи 30 | MEMORY_RELEASE: 'Задача ожидает освобождения памяти (%s использовано)' 31 | STALLED_JOB: 'Замороженная задача' 32 | STALLED_JOB_MSG: 'Задача %s заморожена. Задача была остановлена, войдите в систему для проверки' 33 | START_JOB_TIME: 'Начать задачу в' 34 | TABLE_ADDE: Добавлена 35 | TABLE_MESSAGES: Сообщение 36 | TABLE_NUM_PROCESSED: Выполнено 37 | TABLE_STARTED: Начата 38 | TABLE_START_AFTER: 'Начать после' 39 | TABLE_STATUS: Статус 40 | TABLE_TITLE: Название 41 | TABLE_TOTAL: Итого 42 | TIME_LIMIT: 'Очередь достигла лимита по времени и будет начата заново перед продолжением' 43 | QueuedJobsAdmin: 44 | MENUTITLE: Задачи 45 | ScheduledExecution: 46 | EXECUTE_EVERY: 'Выполнять каждые' 47 | EXECUTE_FREE: 'Запланировано (в формате strtotime после первого выполнения)' 48 | ExecuteEveryDay: День 49 | ExecuteEveryFortnight: 'Две недели' 50 | ExecuteEveryHour: Час 51 | ExecuteEveryMinute: Минута 52 | ExecuteEveryMonth: Месяц 53 | ExecuteEveryWeek: Неделя 54 | ExecuteEveryYear: Год 55 | FIRST_EXECUTION: 'Первое исполнение' 56 | NEXT_RUN_DATE: 'Дата следующего исполнения' 57 | ScheduleTabTitle: Расписание 58 | ScheduledExecutionJob: 59 | Title: 'Запланировано исполнение {title}' 60 | Symbiote\QueuedJobs\Controllers\QueuedJobsAdmin: 61 | CREATE_JOB_TYPE: 'Создать задачу типа' 62 | CREATE_NEW_JOB: 'Создать задачу' 63 | JobsFieldTitle: Задачи 64 | MENUTITLE: Задачи 65 | START_JOB_TIME: 'Начать задачу в' 66 | Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor: 67 | JOB_TYPE: 'Тип задачи' 68 | PLURALNAME: 'Дескрипторы отложенных задач' 69 | SINGULARNAME: 'Дескриптор отложенной задачи' 70 | TABLE_ADDE: Добавлена 71 | TABLE_MESSAGES: Сообщение 72 | TABLE_NUM_PROCESSED: Выполнено 73 | TABLE_STARTED: Начата 74 | TABLE_START_AFTER: 'Начать после' 75 | TABLE_STATUS: Статус 76 | TABLE_TITLE: Заголовок 77 | TABLE_TOTAL: Итого 78 | Symbiote\QueuedJobs\DataObjects\QueuedJobRule: 79 | PLURALNAME: 'Настройки отложенных задач' 80 | SINGULARNAME: 'Настройки отложенной задачи' 81 | Symbiote\QueuedJobs\Extensions\ScheduledExecutionExtension: 82 | EXECUTE_EVERY: 'Выполнять каждые' 83 | EXECUTE_FREE: 'Запланировано (в формате strtotime после первого выполнения)' 84 | ExecuteEveryDay: День 85 | ExecuteEveryFortnight: 'Две недели' 86 | ExecuteEveryHour: Час 87 | ExecuteEveryMinute: Минута 88 | ExecuteEveryMonth: Месяц 89 | ExecuteEveryWeek: Неделя 90 | ExecuteEveryYear: Год 91 | FIRST_EXECUTION: 'Первое исполнение' 92 | NEXT_RUN_DATE: 'Дата следующего исполнения' 93 | ScheduleTabTitle: Расписание 94 | db_ExecuteEvery: 'Выполнять каждые' 95 | Symbiote\QueuedJobs\Jobs\CleanupJob: 96 | Title: 'Стереть старые задания из базы данных' 97 | Symbiote\QueuedJobs\Jobs\DeleteObjectJob: 98 | DELETE_JOB: Удалить 99 | DELETE_OBJ2: 'Удалить {title}' 100 | Symbiote\QueuedJobs\Jobs\GenerateGoogleSitemapJob: 101 | REGENERATE: 'Генерировать Google sitemap .xml файл' 102 | Symbiote\QueuedJobs\Jobs\PublishItemsJob: 103 | Title: 'Опубликовать вложенные объекты {title}' 104 | Symbiote\QueuedJobs\Jobs\ScheduledExecutionJob: 105 | Title: 'Запланировано исполнение {title}' 106 | Symbiote\QueuedJobs\Services\QueuedJobService: 107 | STALLED_JOB: 'Замороженная задача' 108 | TIME_LIMIT: 'Очередь достигла лимита по времени и будет начата заново перед продолжением' 109 | Symbiote\QueuedJobs\Tasks\CreateQueuedJobTask: 110 | Description: 'Задача используется для создания отложенного действия. Укажите тип задачи, укажите опциональный параметр "start" (обрабатывается функцией strtotime) для времени начала.' 111 | Symbiote\QueuedJobs\Tasks\ProcessJobQueueTask: 112 | Description: 'Используется cron для выполнения отложенных задач.' 113 | -------------------------------------------------------------------------------- /lang/de.yml: -------------------------------------------------------------------------------- 1 | de: 2 | CleanupJob: 3 | Title: 'Bereinigt alte Aufträge aus der Datenbank' 4 | CreateQueuedJobTask: 5 | Description: 'Ein Task um geplante Aufträge zu erstellen. Parameter sind: "name". Name der Klasse, "start" (optional): Startzeit für den Auftrag, im strtotime Format' 6 | DeleteObjectJob: 7 | DELETE_JOB: 'Knoten löschen' 8 | DELETE_OBJ2: '{title} löschen' 9 | GenerateSitemapJob: 10 | REGENERATE: 'Generiere Google sitemap.xml Datei' 11 | ProcessJobQueueTask: 12 | Description: 'Genutzt durch einen cron-job zur Ausführung von geplanten Aufträgen.' 13 | PublishItemsJob: 14 | Title: 'Veröffentlich Artikel mit Titel {title}' 15 | QueuedJobDescriptor: 16 | PLURALNAME: 'Beschreibungen der geplanten Aufträge' 17 | SINGULARNAME: 'Beschreibung des geplanten Auftrags' 18 | QueuedJobRule: 19 | PLURALNAME: 'Regeln für den geplanten Auftrag' 20 | SINGULARNAME: 'Regel für den geplanten Auftrag' 21 | QueuedJobs: 22 | CREATE_JOB_TYPE: 'Erstelle einen Auftrag dieses Types' 23 | CREATE_NEW_JOB: 'Neuen Auftrag erstellen' 24 | JOB_EXCEPT: "Auftrag hat einen '%s' Fehler verursacht in %s Zeile %s " 25 | JOB_PAUSED: 'Auftrag pausiert um %s' 26 | JOB_STALLED: 'Auftrag angehalten nach %s versuchen. Bitte überprüfen Sie den Auftrag.' 27 | JOB_TYPE: Auftragstyp 28 | JOB_TYPE_PARAMS: 'Konstruktor-Parameter für die Joberstellung' 29 | JobsFieldTitle: Aufträge 30 | MEMORY_RELEASE: 'Job gibt Speicher frei und wartet (1%s verwendet)' 31 | STALLED_JOB: 'Angehaltener Auftrag' 32 | STALLED_JOB_MSG: 'Der Auftrag %s scheint festzustecken. Er wurde angehalten. Bitte loggen Sie sich ein, um den Auftrag zu überprüfen.' 33 | START_JOB_TIME: 'Auftrag starten um' 34 | TABLE_ADDE: Hinzugefügt 35 | TABLE_MESSAGES: Nachricht 36 | TABLE_NUM_PROCESSED: Fertig 37 | TABLE_STARTED: Gestartet 38 | TABLE_START_AFTER: 'Beginn nach' 39 | TABLE_TITLE: Titel 40 | TABLE_TOTAL: Insgesamt 41 | TIME_LIMIT: 'Die Warteschlange hat das Zeitlimit überschritten und wird neu gestartet' 42 | QueuedJobsAdmin: 43 | MENUTITLE: Aufträge 44 | RunBuildTaskJob: 45 | JOB_TITLE: 'BuildTask {task} ausführen' 46 | ScheduledExecution: 47 | EXECUTE_EVERY: 'Alle ausführen' 48 | EXECUTE_FREE: 'Geplant (in strtotime-format nach dem ersten Ausführen)' 49 | ExecuteEveryDay: Tag 50 | ExecuteEveryFortnight: 'Zwei Wochen' 51 | ExecuteEveryHour: Stunde 52 | ExecuteEveryMonth: Monat 53 | ExecuteEveryWeek: Woche 54 | ExecuteEveryYear: Jahr 55 | FIRST_EXECUTION: 'Erste Ausführung' 56 | NEXT_RUN_DATE: 'Nächstes Ausführungsdatum' 57 | ScheduleTabTitle: Planen 58 | ScheduledExecutionJob: 59 | Title: 'Geplante Ausführung für {title}' 60 | Symbiote\QueuedJobs\Controllers\QueuedJobsAdmin: 61 | CREATE_JOB_TYPE: 'Erstelle einen Auftrag dieses Types' 62 | CREATE_NEW_JOB: 'Neuen Auftrag erstellen' 63 | JobsFieldTitle: Aufträge 64 | MENUTITLE: Aufträge 65 | START_JOB_TIME: 'Auftrag starten um' 66 | Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor: 67 | JOB_TYPE: Auftragstyp 68 | PLURALNAME: 'Beschreibungen der geplanten Aufträge' 69 | SINGULARNAME: 'Beschreibung des geplanten Auftrags' 70 | TABLE_ADDE: Hinzugefügt 71 | TABLE_FINISHED: Abgeschlossen 72 | TABLE_MESSAGES: Nachricht 73 | TABLE_NUM_PROCESSED: Fertig 74 | TABLE_STARTED: Gestartet 75 | TABLE_START_AFTER: 'Beginn nach' 76 | TABLE_TITLE: Titel 77 | TABLE_TOTAL: Insgesamt 78 | Symbiote\QueuedJobs\DataObjects\QueuedJobRule: 79 | PLURALNAME: 'Regeln für den geplanten Auftrag' 80 | SINGULARNAME: 'Regel für den geplanten Auftrag' 81 | Symbiote\QueuedJobs\Extensions\ScheduledExecutionExtension: 82 | EXECUTE_EVERY: 'Alle ausführen' 83 | EXECUTE_FREE: 'Geplant (in strtotime-format nach dem ersten Ausführen)' 84 | ExecuteEveryDay: Tag 85 | ExecuteEveryFortnight: 'Zwei Wochen' 86 | ExecuteEveryHour: Stunde 87 | ExecuteEveryMonth: Monat 88 | ExecuteEveryWeek: Woche 89 | ExecuteEveryYear: Jahr 90 | FIRST_EXECUTION: 'Erste Ausführung' 91 | NEXT_RUN_DATE: 'Nächstes Ausführungsdatum' 92 | ScheduleTabTitle: Planen 93 | db_ExecuteEvery: 'Alle ausführen' 94 | Symbiote\QueuedJobs\Jobs\CleanupJob: 95 | Title: 'Bereinigt alte Aufträge aus der Datenbank' 96 | Symbiote\QueuedJobs\Jobs\DeleteObjectJob: 97 | DELETE_JOB: 'Knoten löschen' 98 | DELETE_OBJ2: '{title} löschen' 99 | Symbiote\QueuedJobs\Jobs\GenerateGoogleSitemapJob: 100 | REGENERATE: 'Generiere Google sitemap.xml Datei' 101 | Symbiote\QueuedJobs\Jobs\PublishItemsJob: 102 | Title: 'Veröffentlich Artikel mit Titel {title}' 103 | Symbiote\QueuedJobs\Jobs\ScheduledExecutionJob: 104 | Title: 'Geplante Ausführung für {title}' 105 | Symbiote\QueuedJobs\Services\QueuedJobService: 106 | STALLED_JOB: 'Angehaltener Auftrag' 107 | TIME_LIMIT: 'Die Warteschlange hat das Zeitlimit überschritten und wird neu gestartet' 108 | Symbiote\QueuedJobs\Tasks\CreateQueuedJobTask: 109 | Description: 'Ein Task um geplante Aufträge zu erstellen. Parameter sind: "name". Name der Klasse, "start" (optional): Startzeit für den Auftrag, im strtotime Format' 110 | Symbiote\QueuedJobs\Tasks\ProcessJobQueueTask: 111 | Description: 'Genutzt durch einen cron-job zur Ausführung von geplanten Aufträgen.' 112 | -------------------------------------------------------------------------------- /lang/pl.yml: -------------------------------------------------------------------------------- 1 | pl: 2 | CleanupJob: 3 | Title: 'Wyczyść stare zadania z bazy danych' 4 | CreateQueuedJobTask: 5 | Description: 'Zadanie zostało użyte do utworzenia zadania w kolejce. Przekaż kolejkę nazw klasy zadania jako parametr "nazwa", przekazuj opcjonalny parametr "start" (analizowany przez strtotime), aby ustawić czas rozpoczęcia zadania.' 6 | DeleteObjectJob: 7 | DELETE_JOB: 'Usuń węzeł' 8 | DELETE_OBJ2: 'Usuń {title}' 9 | GenerateSitemapJob: 10 | REGENERATE: 'Ponownie wygeneruj plik .xml mapy witryny' 11 | ProcessJobQueueTask: 12 | Description: 'Używane przez zadanie cron do uruchamiania zadań oczekujących w kolejce, które należy uruchomić.' 13 | PublishItemsJob: 14 | Title: 'Opublikuj poniższe elementy {title}' 15 | QueuedJobDescriptor: 16 | PLURALNAME: 'Zadania w kolejce' 17 | SINGULARNAME: 'Zadanie w kolejce' 18 | QueuedJobRule: 19 | PLURALNAME: 'Kolejkowe reguły zadań' 20 | SINGULARNAME: 'Kolejkowa reguła zadania' 21 | QueuedJobs: 22 | CREATE_JOB_TYPE: 'Utwórz zadanie typu' 23 | CREATE_NEW_JOB: 'Utwórz nowe zadanie' 24 | JOB_EXCEPT: 'Zadanie spowodowało wyjątek %s w %s w linii %s' 25 | JOB_PAUSED: 'Zadanie zostało wstrzymane via %s' 26 | JOB_STALLED: 'Zadanie wstrzymane po %s próbach - proszę sprawdzić' 27 | JOB_TYPE: 'Typ zadania' 28 | JOB_TYPE_PARAMS: 'Parametry konstruktora dla tworzenia zadania' 29 | JobsFieldTitle: Zadania 30 | MEMORY_RELEASE: 'Pamięć zwalniająca zadania i oczekująca (%s użyto)' 31 | STALLED_JOB: 'Zawieszenie zadania' 32 | STALLED_JOB_MSG: 'Wydaje się, że zadanie o nazwie %s zawiesiło się. Zostało wstrzymane, zaloguj się, aby to sprawdzić' 33 | START_JOB_TIME: 'Rozpocznij zadanie o' 34 | TABLE_ADDE: Dodano 35 | TABLE_MESSAGES: Komunikat 36 | TABLE_NUM_PROCESSED: Gotowe 37 | TABLE_STARTED: Rozpoczęty 38 | TABLE_START_AFTER: 'Zacznij po' 39 | TABLE_TITLE: Tytuł 40 | TABLE_TOTAL: Razem 41 | TIME_LIMIT: 'Kolejka przekroczyła limit czasu i zostanie wznowiona przed kontynuowaniem' 42 | QueuedJobsAdmin: 43 | MENUTITLE: Zadania 44 | RunBuildTaskJob: 45 | JOB_TITLE: 'Uruchom narzędzie {task}' 46 | ScheduledExecution: 47 | EXECUTE_EVERY: 'Wykonaj każdy' 48 | EXECUTE_FREE: 'Zaplanowane (w formacie strtotime od pierwszego wykonania)' 49 | ExecuteEveryDay: Dzień 50 | ExecuteEveryFortnight: 'Dwa tygodnie' 51 | ExecuteEveryHour: Godzina 52 | ExecuteEveryMinute: Minuta 53 | ExecuteEveryMonth: Miesiąc 54 | ExecuteEveryWeek: Tydzień 55 | ExecuteEveryYear: Rok 56 | FIRST_EXECUTION: 'Pierwsze wykonanie' 57 | NEXT_RUN_DATE: 'Następna data uruchomienia' 58 | ScheduleTabTitle: Harmonogram 59 | ScheduledExecutionJob: 60 | Title: 'Zaplanowane wykonanie dla {title}' 61 | Symbiote\QueuedJobs\Controllers\QueuedJobsAdmin: 62 | CREATE_JOB_TYPE: 'Utwórz zadanie typu' 63 | CREATE_NEW_JOB: 'Utwórz nowe zadanie' 64 | JobsFieldTitle: Zadania 65 | MENUTITLE: Zadania 66 | START_JOB_TIME: 'Rozpocznij zadanie o' 67 | Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor: 68 | JOB_TYPE: 'Typ zadania' 69 | PLURALNAME: 'Zadania w kolejce' 70 | SINGULARNAME: 'Zadanie w kolejce' 71 | TABLE_ADDE: Dodano 72 | TABLE_MESSAGES: Komunikat 73 | TABLE_NUM_PROCESSED: Gotowe 74 | TABLE_STARTED: Rozpoczęty 75 | TABLE_START_AFTER: 'Zacznij po' 76 | TABLE_TITLE: Tytuł 77 | TABLE_TOTAL: Razem 78 | Symbiote\QueuedJobs\DataObjects\QueuedJobRule: 79 | PLURALNAME: 'Kolejkowe reguły zadań' 80 | SINGULARNAME: 'Kolejkowa reguła zadania' 81 | Symbiote\QueuedJobs\Extensions\ScheduledExecutionExtension: 82 | EXECUTE_EVERY: 'Wykonaj każdy' 83 | EXECUTE_FREE: 'Zaplanowane (w formacie strtotime od pierwszego wykonania)' 84 | ExecuteEveryDay: Dzień 85 | ExecuteEveryFortnight: 'Dwa tygodnie' 86 | ExecuteEveryHour: Godzina 87 | ExecuteEveryMinute: Minuta 88 | ExecuteEveryMonth: Miesiąc 89 | ExecuteEveryWeek: Tydzień 90 | ExecuteEveryYear: Rok 91 | FIRST_EXECUTION: 'Pierwsze wykonanie' 92 | NEXT_RUN_DATE: 'Następna data uruchomienia' 93 | ScheduleTabTitle: Harmonogram 94 | db_ExecuteEvery: 'Wykonaj każdy' 95 | Symbiote\QueuedJobs\Jobs\CleanupJob: 96 | Title: 'Wyczyść stare zadania z bazy danych' 97 | Symbiote\QueuedJobs\Jobs\DeleteObjectJob: 98 | DELETE_JOB: 'Usuń węzeł' 99 | DELETE_OBJ2: 'Usuń {title}' 100 | Symbiote\QueuedJobs\Jobs\GenerateGoogleSitemapJob: 101 | REGENERATE: 'Ponownie wygeneruj plik .xml mapy witryny' 102 | Symbiote\QueuedJobs\Jobs\PublishItemsJob: 103 | Title: 'Opublikuj poniższe elementy {title}' 104 | Symbiote\QueuedJobs\Jobs\ScheduledExecutionJob: 105 | Title: 'Zaplanowane wykonanie dla {title}' 106 | Symbiote\QueuedJobs\Services\QueuedJobService: 107 | STALLED_JOB: 'Zawieszenie zadania' 108 | TIME_LIMIT: 'Kolejka przekroczyła limit czasu i zostanie wznowiona przed kontynuowaniem' 109 | Symbiote\QueuedJobs\Tasks\CreateQueuedJobTask: 110 | Description: 'Zadanie zostało użyte do utworzenia zadania w kolejce. Przekaż kolejkę nazw klasy zadania jako parametr "nazwa", przekazuj opcjonalny parametr "start" (analizowany przez strtotime), aby ustawić czas rozpoczęcia zadania.' 111 | Symbiote\QueuedJobs\Tasks\ProcessJobQueueTask: 112 | Description: 'Używane przez zadanie cron do uruchamiania zadań oczekujących w kolejce, które należy uruchomić.' 113 | -------------------------------------------------------------------------------- /src/Jobs/DoormanQueuedJobTask.php: -------------------------------------------------------------------------------- 1 | descriptor) { 31 | $this->descriptor = QueuedJobDescriptor::get()->byID($this->descriptor->ID); 32 | } 33 | } 34 | 35 | /** 36 | * @inheritdoc 37 | * 38 | * @return null|int 39 | */ 40 | public function getId() 41 | { 42 | return $this->id; 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | * 48 | * @param int $id 49 | * 50 | * @return $this 51 | */ 52 | public function setId($id) 53 | { 54 | $this->id = $id; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * @return QueuedJobDescriptor 61 | */ 62 | public function getDescriptor() 63 | { 64 | return $this->descriptor; 65 | } 66 | 67 | /** 68 | * @param QueuedJobDescriptor $descriptor 69 | */ 70 | public function __construct(QueuedJobDescriptor $descriptor) 71 | { 72 | $this->descriptor = $descriptor; 73 | } 74 | 75 | public function __serialize(): array 76 | { 77 | return [ 78 | 'descriptor' => $this->descriptor->ID, 79 | ]; 80 | } 81 | 82 | public function __unserialize(array $data): void 83 | { 84 | if (!isset($data['descriptor'])) { 85 | throw new InvalidArgumentException('Malformed data'); 86 | } 87 | $descriptor = QueuedJobDescriptor::get() 88 | ->filter('ID', $data['descriptor']) 89 | ->first(); 90 | if (!$descriptor) { 91 | throw new InvalidArgumentException('Descriptor not found'); 92 | } 93 | $this->descriptor = $descriptor; 94 | } 95 | 96 | /** 97 | * @return string 98 | */ 99 | public function getHandler() 100 | { 101 | return 'DoormanQueuedJobHandler'; 102 | } 103 | 104 | /** 105 | * @return array 106 | */ 107 | public function getData() 108 | { 109 | return array( 110 | 'descriptor' => $this->descriptor, 111 | ); 112 | } 113 | 114 | /** 115 | * @return bool 116 | */ 117 | public function ignoresRules() 118 | { 119 | if ($this->descriptor && $this->descriptor->hasMethod('ignoreRules')) { 120 | return $this->descriptor->ignoreRules(); 121 | } 122 | 123 | return false; 124 | } 125 | 126 | /** 127 | * @return bool 128 | */ 129 | public function stopsSiblings() 130 | { 131 | if ($this->descriptor && $this->descriptor->hasMethod('stopsSiblings')) { 132 | return $this->descriptor->stopsSiblings(); 133 | } 134 | 135 | return false; 136 | } 137 | 138 | /** 139 | * @inheritdoc 140 | * 141 | * @return int 142 | */ 143 | public function getExpiresIn() 144 | { 145 | if ($this->descriptor && $this->descriptor->hasMethod('getExpiresIn')) { 146 | return $this->descriptor->getExpiresIn(); 147 | } 148 | 149 | return -1; 150 | } 151 | 152 | /** 153 | * @inheritdoc 154 | * 155 | * @param int $startedAt 156 | * @return bool 157 | */ 158 | public function shouldExpire($startedAt) 159 | { 160 | if ($this->descriptor && $this->descriptor->hasMethod('shouldExpire')) { 161 | return $this->descriptor->shouldExpire($startedAt); 162 | } 163 | 164 | return true; 165 | } 166 | 167 | /** 168 | * @inheritdoc 169 | * 170 | * @return bool 171 | */ 172 | public function canRunTask() 173 | { 174 | $this->refreshDescriptor(); 175 | 176 | if ($this->descriptor) { 177 | return in_array( 178 | $this->descriptor->JobStatus, 179 | array( 180 | QueuedJob::STATUS_NEW, 181 | QueuedJob::STATUS_INIT, 182 | QueuedJob::STATUS_WAIT 183 | ) 184 | ); 185 | } 186 | 187 | return false; 188 | } 189 | 190 | /** 191 | * @inheritdoc 192 | * 193 | * @return bool 194 | */ 195 | public function isCancelled() 196 | { 197 | $this->refreshDescriptor(); 198 | 199 | // Treat completed jobs as cancelled when it comes to how Doorman handles picking up jobs to run 200 | $cancelledStates = [ 201 | QueuedJob::STATUS_CANCELLED, 202 | QueuedJob::STATUS_COMPLETE, 203 | ]; 204 | 205 | if ($this->descriptor) { 206 | return in_array($this->descriptor->JobStatus, $cancelledStates ?? [], true); 207 | } 208 | 209 | return true; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Extensions/ScheduledExecutionExtension.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | class ScheduledExecutionExtension extends Extension 33 | { 34 | /** 35 | * @var array 36 | */ 37 | private static $db = array( 38 | 'FirstExecution' => 'DBDatetime', 39 | 'ExecuteInterval' => 'Int', 40 | 'ExecuteEvery' => "Enum(',Minute,Hour,Day,Week,Fortnight,Month,Year')", 41 | 'ExecuteFree' => 'Varchar', 42 | ); 43 | 44 | /** 45 | * @var array 46 | */ 47 | private static $defaults = array( 48 | 'ExecuteInterval' => 1, 49 | ); 50 | 51 | /** 52 | * @var array 53 | */ 54 | private static $has_one = array( 55 | 'ScheduledJob' => QueuedJobDescriptor::class, 56 | ); 57 | 58 | /** 59 | * @param FieldList $fields 60 | */ 61 | protected function updateCMSFields(FieldList $fields) 62 | { 63 | $fields->removeByName([ 64 | 'ExecuteInterval', 65 | 'ExecuteEvery', 66 | 'ExecuteFree', 67 | 'FirstExecution' 68 | ]); 69 | 70 | $fields->findOrMakeTab( 71 | 'Root.Schedule', 72 | _t(__CLASS__ . '.ScheduleTabTitle', 'Schedule') 73 | ); 74 | 75 | $fields->addFieldsToTab('Root.Schedule', array( 76 | $dt = DatetimeField::create('FirstExecution', _t(__CLASS__ . '.FIRST_EXECUTION', 'First Execution')), 77 | FieldGroup::create( 78 | NumericField::create('ExecuteInterval', ''), 79 | DropdownField::create( 80 | 'ExecuteEvery', 81 | '', 82 | array( 83 | '' => '', 84 | 'Minute' => _t(__CLASS__ . '.ExecuteEveryMinute', 'Minute'), 85 | 'Hour' => _t(__CLASS__ . '.ExecuteEveryHour', 'Hour'), 86 | 'Day' => _t(__CLASS__ . '.ExecuteEveryDay', 'Day'), 87 | 'Week' => _t(__CLASS__ . '.ExecuteEveryWeek', 'Week'), 88 | 'Fortnight' => _t(__CLASS__ . '.ExecuteEveryFortnight', 'Fortnight'), 89 | 'Month' => _t(__CLASS__ . '.ExecuteEveryMonth', 'Month'), 90 | 'Year' => _t(__CLASS__ . '.ExecuteEveryYear', 'Year'), 91 | ) 92 | ) 93 | )->setTitle(_t(__CLASS__ . '.EXECUTE_EVERY', 'Execute every')), 94 | TextField::create( 95 | 'ExecuteFree', 96 | _t(__CLASS__ . '.EXECUTE_FREE', 'Scheduled (in strtotime format from first execution)') 97 | ) 98 | )); 99 | 100 | if ($this->owner->ScheduledJobID) { 101 | $jobTime = $this->owner->ScheduledJob()->StartAfter; 102 | $fields->addFieldsToTab('Root.Schedule', array( 103 | ReadonlyField::create('NextRunDate', _t(__CLASS__ . '.NEXT_RUN_DATE', 'Next run date'), $jobTime) 104 | )); 105 | } 106 | } 107 | 108 | protected function onBeforeWrite() 109 | { 110 | if ($this->owner->FirstExecution) { 111 | $changed = $this->owner->getChangedFields(); 112 | $changed = ( 113 | isset($changed['FirstExecution']) 114 | || isset($changed['ExecuteInterval']) 115 | || isset($changed['ExecuteEvery']) 116 | || isset($changed['ExecuteFree']) 117 | ); 118 | 119 | if ($changed && $this->owner->ScheduledJobID) { 120 | if ($this->owner->ScheduledJob()->exists()) { 121 | $this->owner->ScheduledJob()->delete(); 122 | } 123 | 124 | $this->owner->ScheduledJobID = 0; 125 | } 126 | 127 | if (!$this->owner->ScheduledJobID) { 128 | $job = new ScheduledExecutionJob($this->owner); 129 | $time = DBDatetime::now()->Rfc2822(); 130 | if ($this->owner->FirstExecution) { 131 | $time = DBDatetime::create()->setValue($this->owner->FirstExecution)->Rfc2822(); 132 | } 133 | 134 | $this->owner->ScheduledJobID = QueuedJobService::singleton() 135 | ->queueJob($job, $time); 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * Define your own version of this method in your data objects to be executed EVERY time 142 | * the scheduled job triggers. 143 | */ 144 | public function onScheduledExecution() 145 | { 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Forms/GridFieldQueuedJobExecute.php: -------------------------------------------------------------------------------- 1 | 'block-media', 27 | 'pause' => 'cancel-circled', 28 | 'resume' => 'sync', 29 | ]; 30 | 31 | /** 32 | * Call back to see if the record's action icon should be shown. 33 | * 34 | * @var callable 35 | */ 36 | protected $viewCheck; 37 | 38 | /** 39 | * @param string $action 40 | * @param callable $check 41 | */ 42 | public function __construct($action = 'execute', $check = null) 43 | { 44 | $this->action = $action; 45 | if (!$check) { 46 | $check = function ($record) { 47 | return $record->JobStatus == QueuedJob::STATUS_WAIT || $record->JobStatus == QueuedJob::STATUS_NEW; 48 | }; 49 | } 50 | 51 | $this->viewCheck = $check; 52 | } 53 | 54 | /** 55 | * Add a column 'Delete' 56 | * 57 | * @param GridField $gridField 58 | * @param array $columns 59 | */ 60 | public function augmentColumns($gridField, &$columns) 61 | { 62 | if (!in_array('Actions', $columns ?? [])) { 63 | $columns[] = 'Actions'; 64 | } 65 | } 66 | 67 | /** 68 | * Return any special attributes that will be used for FormField::createTag() 69 | * 70 | * @param GridField $gridField 71 | * @param DataObject $record 72 | * @param string $columnName 73 | * @return array 74 | */ 75 | public function getColumnAttributes($gridField, $record, $columnName) 76 | { 77 | return array('class' => 'grid-field__col-compact'); 78 | } 79 | 80 | /** 81 | * Add the title 82 | * 83 | * @param GridField $gridField 84 | * @param string $columnName 85 | * @return array 86 | */ 87 | public function getColumnMetadata($gridField, $columnName) 88 | { 89 | if ($columnName == 'Actions') { 90 | return array('title' => ''); 91 | } 92 | } 93 | 94 | /** 95 | * Which columns are handled by this component 96 | * 97 | * @param GridField $gridField 98 | * @return array 99 | */ 100 | public function getColumnsHandled($gridField) 101 | { 102 | return array('Actions'); 103 | } 104 | 105 | /** 106 | * Which GridField actions are this component handling 107 | * 108 | * @param GridField $gridField 109 | * @return array 110 | */ 111 | public function getActions($gridField) 112 | { 113 | return array('execute', 'pause', 'resume'); 114 | } 115 | 116 | /** 117 | * @param GridField $gridField 118 | * @param DataObject $record 119 | * @param string $columnName 120 | * @return string|void - the HTML for the column 121 | */ 122 | public function getColumnContent($gridField, $record, $columnName) 123 | { 124 | $icon = $this->icons[$this->action]; 125 | 126 | if ($this->viewCheck) { 127 | $func = $this->viewCheck; 128 | if (!$func($record)) { 129 | return; 130 | } 131 | } 132 | 133 | $field = GridField_FormAction::create( 134 | $gridField, 135 | 'ExecuteJob' . $record->ID, 136 | false, 137 | $this->action, 138 | array('RecordID' => $record->ID) 139 | ); 140 | 141 | $humanTitle = ucfirst($this->action ?? ''); 142 | /** @phpstan-ignore translation.key (we need the key to be dynamic here) */ 143 | $title = _t(__CLASS__ . '.' . $humanTitle, $humanTitle); 144 | 145 | $field 146 | ->addExtraClass('gridfield-button-job' . $this->action) 147 | ->addExtraClass('btn--icon-md btn--no-text grid-field__icon-action') 148 | ->setAttribute('title', $title) 149 | ->setAttribute('aria-label', $title) 150 | ->setDescription($title); 151 | 152 | // Convert legacy classes into icon references for better accessibility 153 | $field->setIcon(preg_replace('/^font-icon-/', '', $icon)); 154 | 155 | return $field->Field(); 156 | } 157 | 158 | /** 159 | * Handle the actions and apply any changes to the GridField 160 | * 161 | * @param GridField $gridField 162 | * @param string $actionName 163 | * @param mixed $arguments 164 | * @param array $data - form data 165 | * @return void 166 | */ 167 | public function handleAction(GridField $gridField, $actionName, $arguments, $data) 168 | { 169 | $actions = $this->getActions(null); 170 | if (in_array($actionName, $actions ?? [])) { 171 | $item = $gridField->getList()->byID($arguments['RecordID']); 172 | if (!$item) { 173 | return; 174 | } 175 | $item->$actionName(); 176 | Requirements::clear(); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Jobs/CleanupJob.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class CleanupJob extends AbstractQueuedJob 20 | { 21 | use Configurable; 22 | 23 | /** 24 | * How we will determine "stale" 25 | * Possible values: age, number 26 | * @config 27 | * @var string 28 | */ 29 | private static $cleanup_method = "age"; 30 | 31 | /** 32 | * Value associated with cleanupMethod 33 | * age => days, number => integer 34 | * @config 35 | * @var integer 36 | */ 37 | private static $cleanup_value = 30; 38 | 39 | /** 40 | * Which JobStatus values are OK to be deleted 41 | * @config 42 | * @var array 43 | */ 44 | private static $cleanup_statuses = array( 45 | "Complete", 46 | "Broken", 47 | // "Initialising", 48 | // "Running", 49 | // "New", 50 | // "Paused", 51 | // "Cancelled", 52 | // "Waiting", 53 | ); 54 | 55 | /** 56 | * Database query limit 57 | * 58 | * @config 59 | * @var integer 60 | */ 61 | private static $query_limit = 100000; 62 | 63 | /** 64 | * Check whether is enabled or not for BC 65 | * @config 66 | * @var boolean 67 | */ 68 | private static $is_enabled = false; 69 | 70 | /** 71 | * Defines the title of the job 72 | * @return string 73 | */ 74 | public function getTitle() 75 | { 76 | return _t( 77 | __CLASS__ . '.Title', 78 | "Clean up old jobs from the database" 79 | ); 80 | } 81 | 82 | /** 83 | * Set immediacy of job 84 | * @return int 85 | */ 86 | public function getJobType() 87 | { 88 | $this->totalSteps = '1'; 89 | return QueuedJob::IMMEDIATE; 90 | } 91 | 92 | /** 93 | * Clear out stale jobs based on the cleanup values 94 | */ 95 | public function process() 96 | { 97 | // construct limit statement if query_limit is valid int value 98 | $limit = ''; 99 | $query_limit = $this->config()->get('query_limit'); 100 | if (is_numeric($query_limit) && $query_limit >= 0) { 101 | $limit = ' LIMIT ' . ((int)$query_limit); 102 | } 103 | 104 | $statusList = implode('\', \'', $this->config()->get('cleanup_statuses')); 105 | switch ($this->config()->get('cleanup_method')) { 106 | // If Age, we need to get jobs that are at least n days old 107 | case "age": 108 | $cutOff = date( 109 | "Y-m-d H:i:s", 110 | strtotime(DBDatetime::now() . 111 | " - " . 112 | $this->config()->cleanup_value . 113 | " days") 114 | ); 115 | $stale = DB::query( 116 | 'SELECT "ID" 117 | FROM "QueuedJobDescriptor" 118 | WHERE "JobStatus" 119 | IN (\'' . $statusList . '\') 120 | AND "LastEdited" < \'' . $cutOff . '\'' . $limit 121 | ); 122 | $staleJobs = $stale->column("ID"); 123 | break; 124 | // If Number, we need to save n records, then delete from the rest 125 | case "number": 126 | $fresh = DB::query( 127 | 'SELECT "ID" 128 | FROM "QueuedJobDescriptor" 129 | ORDER BY "LastEdited" 130 | ASC LIMIT ' . $this->config()->cleanup_value 131 | ); 132 | $freshJobIDs = implode('\', \'', $fresh->column("ID")); 133 | 134 | $stale = DB::query( 135 | 'SELECT "ID" 136 | FROM "QueuedJobDescriptor" 137 | WHERE "ID" 138 | NOT IN (\'' . $freshJobIDs . '\') 139 | AND "JobStatus" 140 | IN (\'' . $statusList . '\')' . $limit 141 | ); 142 | $staleJobs = $stale->column("ID"); 143 | break; 144 | default: 145 | $this->addMessage("Incorrect configuration values set. Cleanup ignored"); 146 | $this->isComplete = true; 147 | return; 148 | } 149 | if (empty($staleJobs)) { 150 | $this->addMessage("No jobs to clean up."); 151 | $this->isComplete = true; 152 | $this->reenqueue(); 153 | return; 154 | } 155 | $numJobs = count($staleJobs ?? []); 156 | $staleJobs = implode('\', \'', $staleJobs); 157 | DB::query('DELETE FROM "QueuedJobDescriptor" 158 | WHERE "ID" 159 | IN (\'' . $staleJobs . '\')'); 160 | $this->addMessage($numJobs . " jobs cleaned up."); 161 | // let's make sure there is a cleanupJob in the queue 162 | $this->reenqueue(); 163 | $this->isComplete = true; 164 | } 165 | 166 | private function reenqueue() 167 | { 168 | if ($this->config()->get('is_enabled')) { 169 | $this->addMessage("Queueing the next Cleanup Job."); 170 | $cleanup = Injector::inst()->create(CleanupJob::class); 171 | QueuedJobService::singleton()->queueJob( 172 | $cleanup, 173 | DBDatetime::create()->setValue(DBDatetime::now()->getTimestamp() + 86400)->Rfc2822() 174 | ); 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Tasks/Engines/DoormanRunner.php: -------------------------------------------------------------------------------- 1 | defaultRules = $rules; 60 | return $this; 61 | } 62 | 63 | /** 64 | * @return array List of rules 65 | */ 66 | public function getDefaultRules() 67 | { 68 | return $this->defaultRules; 69 | } 70 | 71 | /** 72 | * Run tasks on the given queue 73 | * 74 | * @param string $queue 75 | */ 76 | public function runQueue($queue) 77 | { 78 | $service = $this->getService(); 79 | $logger = $service->getLogger(); 80 | 81 | // check if queue can be processed 82 | if ($service->isAtMaxJobs()) { 83 | $logger->info('Not processing queue as jobs are at max initialisation limit.'); 84 | 85 | return; 86 | } 87 | 88 | // split jobs out into multiple tasks... 89 | 90 | $manager = Injector::inst()->create(ProcessManager::class); 91 | $manager->setWorker( 92 | sprintf( 93 | '%s/vendor/bin/sake %s', 94 | BASE_PATH, 95 | $this->getChildRunner() 96 | ) 97 | ); 98 | 99 | $logPath = Environment::getEnv('SS_DOORMAN_LOGPATH'); 100 | if ($logPath && is_dir($logPath ?? '') && is_writable($logPath ?? '')) { 101 | $manager->setLogPath($logPath); 102 | } 103 | 104 | $phpBinary = Environment::getEnv('SS_DOORMAN_PHPBINARY'); 105 | if ($phpBinary && is_executable($phpBinary)) { 106 | $manager->setBinary($phpBinary); 107 | } 108 | 109 | // Assign default rules 110 | $defaultRules = $this->getDefaultRules(); 111 | 112 | if ($defaultRules) { 113 | foreach ($defaultRules as $rule) { 114 | if (!$rule) { 115 | continue; 116 | } 117 | 118 | $manager->addRule($rule); 119 | } 120 | } 121 | 122 | $tickCount = 0; 123 | $maxTicks = $this->getMaxTicks(); 124 | $descriptor = $service->getNextPendingJob($queue); 125 | 126 | while ($manager->tick() || $descriptor) { 127 | if ($service->isMaintenanceLockActive()) { 128 | $logger->info('Skipped queued job descriptor since maintenance lock is active.'); 129 | 130 | return; 131 | } 132 | 133 | if ($maxTicks > 0 && $tickCount >= $maxTicks) { 134 | $logger->info(sprintf('Tick count has hit max ticks (%d)', $maxTicks)); 135 | 136 | return; 137 | } 138 | 139 | if ($service->isAtMaxJobs()) { 140 | $logger->info( 141 | sprintf( 142 | 'Not processing queue as all job are at max limit. %s', 143 | ClassInfo::shortName($service) 144 | ) 145 | ); 146 | } elseif ($descriptor) { 147 | $logger->info(sprintf('Next pending job is: %d', $descriptor->ID)); 148 | $this->logDescriptorStatus($descriptor, $queue); 149 | 150 | if ($descriptor instanceof QueuedJobDescriptor) { 151 | $descriptor->JobStatus = QueuedJob::STATUS_INIT; 152 | $descriptor->write(); 153 | 154 | $manager->addTask(new DoormanQueuedJobTask($descriptor)); 155 | } 156 | } else { 157 | $logger->info('Next pending job could NOT be found or lock could NOT be obtained.'); 158 | } 159 | 160 | $tickCount += 1; 161 | sleep($this->getTickInterval() ?? 0); 162 | $descriptor = $service->getNextPendingJob($queue); 163 | } 164 | } 165 | 166 | /** 167 | * Override this method if you need a dynamic value for the configuration, for example CMS setting 168 | * 169 | * @return int 170 | */ 171 | protected function getMaxTicks(): int 172 | { 173 | return (int) $this->config()->get('max_ticks'); 174 | } 175 | 176 | /** 177 | * Override this method if you need a dynamic value for the configuration, for example CMS setting 178 | * 179 | * @return int 180 | */ 181 | protected function getTickInterval(): int 182 | { 183 | return (int) $this->config()->get('tick_interval'); 184 | } 185 | 186 | /** 187 | * Override this method if you need a dynamic value for the configuration, for example CMS setting 188 | * 189 | * @return string 190 | */ 191 | protected function getChildRunner(): string 192 | { 193 | return (string) $this->config()->get('child_runner'); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Controllers/QueuedTaskRunner.php: -------------------------------------------------------------------------------- 1 | 'queueTask', 33 | ]; 34 | 35 | private static array $allowed_actions = [ 36 | 'queueTask', 37 | ]; 38 | 39 | private static array $css = [ 40 | 'symbiote/silverstripe-queuedjobs:client/styles/task-runner.css', 41 | ]; 42 | 43 | /** 44 | * Tasks on this list will not be available to run via the jobs queue 45 | */ 46 | private static array $task_blacklist = [ 47 | ProcessJobQueueTask::class, 48 | CreateQueuedJobTask::class, 49 | DeleteAllJobsTask::class, 50 | ]; 51 | 52 | /** 53 | * Tasks on this list will be available to be run only via jobs queue 54 | */ 55 | private static array $queued_only_tasks = []; 56 | 57 | public function index() 58 | { 59 | $baseUrl = Director::absoluteBaseURL(); 60 | $tasks = $this->getTasks(); 61 | 62 | $blacklist = (array) $this->config()->get('task_blacklist'); 63 | $queuedOnlyList = (array) $this->config()->get('queued_only_tasks'); 64 | $backlistedTasks = []; 65 | $queuedOnlyTasks = []; 66 | 67 | $taskList = ArrayList::create(); 68 | 69 | // universal tasks 70 | foreach ($tasks as $task) { 71 | if (in_array($task['class'], $blacklist ?? [])) { 72 | $backlistedTasks[] = $task; 73 | 74 | continue; 75 | } 76 | 77 | if (in_array($task['class'], $queuedOnlyList ?? [])) { 78 | $queuedOnlyTasks[] = $task; 79 | 80 | continue; 81 | } 82 | 83 | $taskList->push(ArrayData::create([ 84 | 'QueueLink' => Controller::join_links($baseUrl, 'dev/tasks/queue', $task['segment']), 85 | 'TaskLink' => Controller::join_links($baseUrl, 'dev/tasks', $task['segment']), 86 | 'Title' => $task['title'], 87 | 'Description' => $task['description'], 88 | 'Type' => 'universal', 89 | 'Parameters' => $task['parameters'], 90 | 'Help' => $task['help'], 91 | ])); 92 | } 93 | 94 | // Non-queueable tasks 95 | foreach ($backlistedTasks as $task) { 96 | $taskList->push(ArrayData::create([ 97 | 'TaskLink' => Controller::join_links($baseUrl, 'dev/tasks', $task['segment']), 98 | 'Title' => $task['title'], 99 | 'Description' => $task['description'], 100 | 'Type' => 'immediate', 101 | 'Parameters' => $task['parameters'], 102 | 'Help' => $task['help'], 103 | ])); 104 | } 105 | 106 | // Queue only tasks 107 | foreach ($queuedOnlyTasks as $task) { 108 | $taskList->push(ArrayData::create([ 109 | 'QueueLink' => Controller::join_links($baseUrl, 'dev/tasks/queue', $task['segment']), 110 | 'Title' => $task['title'], 111 | 'Description' => $task['description'], 112 | 'Type' => 'queue-only', 113 | 'Parameters' => $task['parameters'], 114 | 'Help' => $task['help'], 115 | ])); 116 | } 117 | 118 | $renderer = DebugView::create(); 119 | $header = $renderer->renderHeader(); 120 | $header = $this->addCssToHeader($header); 121 | 122 | $data = [ 123 | 'Tasks' => $taskList, 124 | 'Header' => $header, 125 | 'Footer' => $renderer->renderFooter(), 126 | 'Info' => $renderer->renderInfo('SilverStripe Development Tools: Tasks (QueuedJobs version)', $baseUrl), 127 | ]; 128 | 129 | return ModelData::create()->renderWith(static::class, $data); 130 | } 131 | 132 | 133 | /** 134 | * Adds a RunBuildTaskJob to the job queue for a given task 135 | * 136 | * @param HTTPRequest $request 137 | */ 138 | public function queueTask($request) 139 | { 140 | $name = $request->param('TaskName'); 141 | $tasks = $this->getTasks(); 142 | 143 | $variables = $request->getVars(); 144 | unset($variables['url']); 145 | unset($variables['flush']); 146 | unset($variables['flushtoken']); 147 | unset($variables['isDev']); 148 | $querystring = http_build_query($variables ?? []); 149 | 150 | $title = function ($content) { 151 | printf(Director::is_cli() ? "%s\n\n" : '

%s

', $content); 152 | }; 153 | 154 | $message = function ($content) { 155 | printf(Director::is_cli() ? "%s\n" : '

%s

', $content); 156 | }; 157 | 158 | foreach ($tasks as $task) { 159 | if ($task['segment'] == $name) { 160 | /** @var BuildTask $inst */ 161 | $inst = Injector::inst()->create($task['class']); 162 | if (!$inst->isEnabled()) { 163 | $message('The task is disabled'); 164 | return; 165 | } 166 | 167 | $title(sprintf('Queuing Task %s', $inst->getTitle())); 168 | 169 | $job = new RunBuildTaskJob($task['class'], $querystring); 170 | $jobID = Injector::inst()->get(QueuedJobService::class)->queueJob($job); 171 | 172 | $message('Done: queued with job ID ' . $jobID); 173 | $adminLink = Controller::join_links( 174 | Director::baseURL(), 175 | AdminRootController::config()->get('url_base'), 176 | 'queuedjobs', 177 | str_replace('\\', '-', QueuedJobDescriptor::class) 178 | ); 179 | $message("Visit queued jobs admin to see job status"); 180 | return; 181 | } 182 | } 183 | 184 | $message(sprintf('The build task "%s" could not be found', Convert::raw2xml($name))); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /lang/eo.yml: -------------------------------------------------------------------------------- 1 | eo: 2 | CleanupJob: 3 | Title: 'Purigi malnovajn taskojn el la datumbazo' 4 | CreateQueuedJobTask: 5 | Description: 'Tasko uzota por krei atendovican taskon. Pasu la klasnomon de la atendovican tasko kiel la parametron "nomo", pasu malnepran parametron "starto" (analizeblan de strtotime) por agordi startotempon por la tasko.' 6 | DeleteObjectJob: 7 | DELETE_JOB: 'Forigi nodon' 8 | DELETE_OBJ2: 'Forigi je {title}' 9 | GenerateSitemapJob: 10 | REGENERATE: 'Regeneri xml-dosieron de Guglo-paĝarmapo' 11 | ProcessJobQueueTask: 12 | Description: "Uzebla per 'cron'-tasko por plenumi envicigitajn taskojn rulotajn." 13 | PublishItemsJob: 14 | Title: 'Publikigi erojn sub {title}' 15 | QueuedJobDescriptor: 16 | PLURALNAME: 'Atendovicaj priaĵoj' 17 | SINGULARNAME: 'Atendovica priaĵo' 18 | QueuedJobRule: 19 | PLURALNAME: 'Atendovicaj reguloj' 20 | SINGULARNAME: 'Atendovica regulo' 21 | QueuedJobs: 22 | CREATE_JOB_TYPE: 'Krei taskon de la tipo' 23 | CREATE_NEW_JOB: 'Krei novan taskon' 24 | JOB_EXCEPT: 'Tasko kaŭzis escepton %s en %s ĉe linio %s' 25 | JOB_PAUSED: 'Tasko paŭzis ĉe %s' 26 | JOB_STALLED: 'Tasko haltis post %s provoj - bonvole kontrolu' 27 | JOB_TYPE: 'Tipo de tasko' 28 | JOB_TYPE_PARAMS: 'Konstruaj parametroj por taskokreo' 29 | JobsFieldTitle: Taskoj 30 | MEMORY_RELEASE: 'Tasko liberigas memoron kaj atendas (%s uzita)' 31 | STALLED_JOB: 'Haltinta tasko' 32 | STALLED_JOB_MSG: 'Ŝajnas ke haltis tasko nomita %s. Ĝi estas paŭzigita. Bonvolu ensaluti por kontroli ĝin.' 33 | START_JOB_TIME: 'Startigi taskon je' 34 | TABLE_ADDE: Aldonita 35 | TABLE_MESSAGES: Mesaĝo 36 | TABLE_NUM_PROCESSED: Farita 37 | TABLE_STARTED: Startis 38 | TABLE_START_AFTER: 'Startigi post' 39 | TABLE_STATUS: Stato 40 | TABLE_TITLE: Titolo 41 | TABLE_TOTAL: Totalo 42 | TIME_LIMIT: 'Atendovico pasis la tempolimon kaj restartos antaŭ ol daŭri' 43 | QueuedJobsAdmin: 44 | MENUTITLE: Taskoj 45 | RunBuildTaskJob: 46 | JOB_TITLE: 'Ruli je KonstruiTaskon' 47 | ScheduledExecution: 48 | EXECUTE_EVERY: 'Plenumi ĉiun' 49 | EXECUTE_FREE: 'Planita (en strtotime-formato ek de unua plenumo)' 50 | ExecuteEveryDay: Tago 51 | ExecuteEveryFortnight: 'Du semajnoj' 52 | ExecuteEveryHour: Horo 53 | ExecuteEveryMinute: Minuto 54 | ExecuteEveryMonth: Monato 55 | ExecuteEveryWeek: Semajno 56 | ExecuteEveryYear: Jaro 57 | FIRST_EXECUTION: 'Unua plenumo' 58 | NEXT_RUN_DATE: 'Sekva ruldato' 59 | ScheduleTabTitle: Plano 60 | ScheduledExecutionJob: 61 | Title: 'Planita plenumo por {title}' 62 | Symbiote\QueuedJobs\Controllers\QueuedJobsAdmin: 63 | CREATE_JOB_TYPE: 'Krei taskon de la tipo' 64 | CREATE_NEW_JOB: 'Krei novan taskon' 65 | JOB_TYPE_PARAMS: 'Konstruaj parametroj por taskokreo (je unu en ĉiu linio)' 66 | JobsFieldTitle: Taskoj 67 | MENUTITLE: Taskoj 68 | QueuedJobSuccess: 'Sukcese envicigis taskon' 69 | START_JOB_TIME: 'Startigi taskon je' 70 | Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor: 71 | JOB_TYPE: 'Tipo de tasko' 72 | PLURALNAME: 'Atendovicaj priaĵoj' 73 | PLURALS: 74 | one: 'Unu atendovica priaĵo' 75 | other: '{count} atendovicaj priaĵoj' 76 | SINGULARNAME: 'Atendovica priaĵo' 77 | TABLE_ADDE: Aldonita 78 | TABLE_FINISHED: Finis 79 | TABLE_MESSAGES: Mesaĝo 80 | TABLE_NUM_PROCESSED: Farita 81 | TABLE_STARTED: Startis 82 | TABLE_START_AFTER: 'Startigi post' 83 | TABLE_STATUS: Stato 84 | TABLE_TITLE: Titolo 85 | TABLE_TOTAL: Totalo 86 | TYPE_IMMEDIATE: tuja 87 | TYPE_LARGE: Granda 88 | TYPE_QUEUED: Envicigita 89 | db_Expiry: Eksvalidiĝo 90 | db_Implementation: Realigo 91 | db_JobFinished: 'Tasko finita' 92 | db_JobRestarted: 'Tasko restartita' 93 | db_JobStarted: 'Tasko startita' 94 | db_JobStatus: 'Stato de tasko' 95 | db_JobTitle: 'Titolo de tasko' 96 | db_JobType: 'Tipo de tasko' 97 | db_LastProcessedCount: 'Nombro da laste traktitaj' 98 | db_NotifiedBroken: 'Atentigis rompita' 99 | db_ResumeCounts: 'Restarti nombradon' 100 | db_SavedJobData: 'Datumoj pri konservita tasko' 101 | db_SavedJobMessages: 'Mesaĝoj pri konservita tasko' 102 | db_Signature: Subskribo 103 | db_StartAfter: 'Startigi post' 104 | db_StepsProcessed: 'Paŝoj traktitaj' 105 | db_TotalSteps: 'Sumo da paŝoj' 106 | db_Worker: Laboranto 107 | db_WorkerCount: 'Nombro da laborantoj' 108 | has_one_RunAs: 'Ruligi kiel' 109 | Symbiote\QueuedJobs\DataObjects\QueuedJobRule: 110 | PLURALNAME: 'Atendovicaj reguloj' 111 | PLURALS: 112 | one: 'Unu atendovica priaĵo' 113 | other: '{count} atendovicaj priaĵoj' 114 | SINGULARNAME: 'Atendovica regulo' 115 | db_Handler: Traktilo 116 | db_MaximumMemoryUsage: 'Maksimuma memoruzado' 117 | db_MaximumProcessorUsage: 'Maksimuma procesiluzado' 118 | db_MaximumSiblingMemoryUsage: 'Maksimuma gefrata memoruzado' 119 | db_MaximumSiblingProcessorUsage: 'Maksimuma gefrata procesiluzado' 120 | db_MinimumMemoryUsage: 'Minimuma memoruzado' 121 | db_MinimumProcessorUsage: 'Minimuma procesiluzado' 122 | db_MinimumSiblingMemoryUsage: 'Minimuma gefrata memoruzado' 123 | db_MinimumSiblingProcessorUsage: 'Minimuma gefrata procesiluzado' 124 | db_Processes: Procesoj 125 | Symbiote\QueuedJobs\Extensions\MaintenanceLockExtension: 126 | LOCK_DESCRIPTION: 'Ebligi ke mastrumila ŝlosilo preventi ŝtartigi novajn envicigitajn taskojn' 127 | LOCK_ENABLED: 'Mastrumila ŝlosilo ŝaltita' 128 | Symbiote\QueuedJobs\Extensions\ScheduledExecutionExtension: 129 | EXECUTE_EVERY: 'Plenumi ĉiun' 130 | EXECUTE_FREE: 'Planita (en strtotime-formato ek de unua plenumo)' 131 | ExecuteEveryDay: Tago 132 | ExecuteEveryFortnight: 'Du semajnoj' 133 | ExecuteEveryHour: Horo 134 | ExecuteEveryMinute: Minuto 135 | ExecuteEveryMonth: Monato 136 | ExecuteEveryWeek: Semajno 137 | ExecuteEveryYear: Jaro 138 | FIRST_EXECUTION: 'Unua plenumo' 139 | NEXT_RUN_DATE: 'Sekva ruldato' 140 | ScheduleTabTitle: Plano 141 | db_ExecuteEvery: 'Plenumi ĉiun' 142 | db_ExecuteFree: 'Plenumi libere' 143 | db_ExecuteInterval: 'Plenuma intervalo' 144 | db_FirstExecution: 'Unua plenumo' 145 | has_one_ScheduledJob: 'Planita tasko' 146 | Symbiote\QueuedJobs\Jobs\CleanupJob: 147 | Title: 'Purigi malnovajn taskojn el la datumbazo' 148 | Symbiote\QueuedJobs\Jobs\DeleteObjectJob: 149 | DELETE_JOB: 'Forigi nodon' 150 | DELETE_OBJ2: 'Forigi je {title}' 151 | Symbiote\QueuedJobs\Jobs\GenerateGoogleSitemapJob: 152 | REGENERATE: 'Regeneri xml-dosieron de Guglo-paĝarmapo' 153 | Symbiote\QueuedJobs\Jobs\PublishItemsJob: 154 | Title: 'Publikigi erojn sub {title}' 155 | Symbiote\QueuedJobs\Jobs\ScheduledExecutionJob: 156 | Title: 'Planita plenumo por {title}' 157 | Symbiote\QueuedJobs\Services\QueuedJobService: 158 | JOB_EXCEPT: 'Tasko kaŭzis escepton {message} en {file} ĉe linio {line}' 159 | JOB_PAUSED: 'Tasko paŭzis je {time}' 160 | JOB_STALLED: 'Tasko haltis post {attempts} provoj - bonvole kontrolu' 161 | MEMORY_RELEASE: 'Tasko liberigas memoron kaj atendas ({used} uzita)' 162 | STALLED_JOB: 'Haltinta tasko' 163 | STALLED_JOB_MSG: 'Ŝajnas ke haltis tasko nomita {name}. Ĝi estas paŭzigita. Bonvolu ensaluti por kontroli ĝin.' 164 | STALLED_JOB_RESTART_MSG: 'Ŝajnas ke haltis tasko nomita {name}. Ĝi haltos kaj restartiĝos. Bonvolu ensaluti por kontroli ke ĝi daŭris' 165 | TIME_LIMIT: 'Atendovico pasis la tempolimon kaj restartos antaŭ ol daŭri' 166 | Symbiote\QueuedJobs\Tasks\CheckJobHealthTask: 167 | Description: 'Tasko kontrolis la sanon de taskoj "rulantaj". Enigi specifan atendovicon kiel la parametro "atendovico"; alie la "envicigita" atendovico kontroliĝos.' 168 | Symbiote\QueuedJobs\Tasks\CreateQueuedJobTask: 169 | Description: 'Tasko uzota por krei atendovican taskon. Pasu la klasnomon de la atendovican tasko kiel la parametron "nomo", pasu malnepran parametron "starto" (analizeblan de strtotime) por agordi startotempon por la tasko.' 170 | Symbiote\QueuedJobs\Tasks\ProcessJobQueueTask: 171 | Description: "Uzebla per 'cron'-tasko por plenumi envicigitajn taskojn rulotajn." 172 | -------------------------------------------------------------------------------- /lang/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | CleanupJob: 3 | Title: 'Clean up old jobs from the database' 4 | CreateQueuedJobTask: 5 | Description: 'A task used to create a queued job. Pass the queued job class name as the "name" parameter, pass an optional "start" parameter (parseable by strtotime) to set a start time for the job.' 6 | DeleteObjectJob: 7 | DELETE_JOB: 'Delete node' 8 | DELETE_OBJ2: 'Delete {title}' 9 | GenerateSitemapJob: 10 | REGENERATE: 'Regenerate Google sitemap .xml file' 11 | ProcessJobQueueTask: 12 | Description: 'Used via a cron job to execute queued jobs that need to be run.' 13 | PublishItemsJob: 14 | Title: 'Publish items beneath {title}' 15 | QueuedJobDescriptor: 16 | PLURALNAME: 'Queued Job Descriptors' 17 | SINGULARNAME: 'Queued Job Descriptor' 18 | QueuedJobRule: 19 | PLURALNAME: 'Queued Job Rules' 20 | SINGULARNAME: 'Queued Job Rule' 21 | QueuedJobs: 22 | BROKEN_JOBS: 'Broken job(s)' 23 | BROKEN_JOBS_MSG: 'The following job(s) appear to be broken.' 24 | CREATE_JOB_TYPE: 'Create job of type' 25 | CREATE_NEW_JOB: 'Create new job' 26 | JOB_EXCEPT: 'Job caused exception %s in %s at line %s' 27 | JOB_PAUSED: 'Job paused at %s' 28 | JOB_STALLED: 'Job stalled after %s attempts - please check' 29 | JOB_TYPE: 'Job Type' 30 | JOB_TYPE_PARAMS: 'Constructor parameters for job creation' 31 | JobsFieldTitle: Jobs 32 | MEMORY_RELEASE: 'Job releasing memory and waiting (%s used)' 33 | STALLED_JOB: 'Stalled job' 34 | STALLED_JOB_MSG: 'A job named %s appears to have stalled. It has been paused, please login to check it' 35 | START_JOB_TIME: 'Start job at' 36 | TABLE_ADDE: Added 37 | TABLE_MESSAGES: Message 38 | TABLE_NUM_PROCESSED: Done 39 | TABLE_STARTED: Started 40 | TABLE_START_AFTER: 'Start After' 41 | TABLE_STATUS: Status 42 | TABLE_TITLE: Title 43 | TABLE_TOTAL: Total 44 | TIME_LIMIT: 'Queue has passed time limit and will restart before continuing' 45 | QueuedJobsAdmin: 46 | MENUTITLE: Jobs 47 | RunBuildTaskJob: 48 | JOB_TITLE: 'Run BuildTask {task}' 49 | ScheduledExecution: 50 | EXECUTE_EVERY: 'Execute every' 51 | EXECUTE_FREE: 'Scheduled (in strtotime format from first execution)' 52 | ExecuteEveryDay: Day 53 | ExecuteEveryFortnight: Fortnight 54 | ExecuteEveryHour: Hour 55 | ExecuteEveryMinute: Minute 56 | ExecuteEveryMonth: Month 57 | ExecuteEveryWeek: Week 58 | ExecuteEveryYear: Year 59 | FIRST_EXECUTION: 'First Execution' 60 | NEXT_RUN_DATE: 'Next run date' 61 | ScheduleTabTitle: Schedule 62 | ScheduledExecutionJob: 63 | Title: 'Scheduled execution for {title}' 64 | Symbiote\QueuedJobs\Controllers\QueuedJobsAdmin: 65 | CREATE_JOB_TYPE: 'Create job of type' 66 | CREATE_NEW_JOB: 'Create new job' 67 | JOB_TYPE_PARAMS: 'Constructor parameters for job creation (one per line)' 68 | JobsFieldTitle: Jobs 69 | MENUTITLE: Jobs 70 | QueuedJobSuccess: 'Successfully queued job' 71 | START_JOB_TIME: 'Start job at' 72 | Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor: 73 | JOB_TYPE: 'Job Type' 74 | PLURALNAME: 'Queued Job Descriptors' 75 | PLURALS: 76 | one: 'A Queued Job Descriptor' 77 | other: '{count} Queued Job Descriptors' 78 | SINGULARNAME: 'Queued Job Descriptor' 79 | TABLE_ADDE: Added 80 | TABLE_FINISHED: Finished 81 | TABLE_MESSAGES: Message 82 | TABLE_NUM_PROCESSED: Done 83 | TABLE_STARTED: Started 84 | TABLE_START_AFTER: 'Start After' 85 | TABLE_STATUS: Status 86 | TABLE_TITLE: Title 87 | TABLE_TOTAL: Total 88 | TYPE_IMMEDIATE: Immediate 89 | TYPE_LARGE: Large 90 | TYPE_QUEUED: Queued 91 | db_Expiry: Expiry 92 | db_Implementation: Implementation 93 | db_JobFinished: 'Job finished' 94 | db_JobRestarted: 'Job restarted' 95 | db_JobStarted: 'Job started' 96 | db_JobStatus: 'Job status' 97 | db_JobTitle: 'Job title' 98 | db_JobType: 'Job type' 99 | db_LastProcessedCount: 'Last processed count' 100 | db_NotifiedBroken: 'Notified broken' 101 | db_ResumeCounts: 'Resume counts' 102 | db_SavedJobData: 'Saved job data' 103 | db_SavedJobMessages: 'Saved job messages' 104 | db_Signature: Signature 105 | db_StartAfter: 'Start after' 106 | db_StepsProcessed: 'Steps processed' 107 | db_TotalSteps: 'Total steps' 108 | db_Worker: Worker 109 | db_WorkerCount: 'Worker count' 110 | has_one_RunAs: 'Run as' 111 | Symbiote\QueuedJobs\DataObjects\QueuedJobRule: 112 | PLURALNAME: 'Queued Job Rules' 113 | PLURALS: 114 | one: 'A Queued Job Rule' 115 | other: '{count} Queued Job Rules' 116 | SINGULARNAME: 'Queued Job Rule' 117 | db_Handler: Handler 118 | db_MaximumMemoryUsage: 'Maximum memory usage' 119 | db_MaximumProcessorUsage: 'Maximum processor usage' 120 | db_MaximumSiblingMemoryUsage: 'Maximum sibling memory usage' 121 | db_MaximumSiblingProcessorUsage: 'Maximum sibling processor usage' 122 | db_MinimumMemoryUsage: 'Minimum memory usage' 123 | db_MinimumProcessorUsage: 'Minimum processor usage' 124 | db_MinimumSiblingMemoryUsage: 'Minimum sibling memory usage' 125 | db_MinimumSiblingProcessorUsage: 'Minimum sibling processor usage' 126 | db_Processes: Processes 127 | Symbiote\QueuedJobs\Extensions\MaintenanceLockExtension: 128 | LOCK_DESCRIPTION: 'Enable maintenance lock to prevent new queued jobs from being started' 129 | LOCK_ENABLED: 'Maintenance Lock Enabled' 130 | Symbiote\QueuedJobs\Extensions\ScheduledExecutionExtension: 131 | EXECUTE_EVERY: 'Execute every' 132 | EXECUTE_FREE: 'Scheduled (in strtotime format from first execution)' 133 | ExecuteEveryDay: Day 134 | ExecuteEveryFortnight: Fortnight 135 | ExecuteEveryHour: Hour 136 | ExecuteEveryMinute: Minute 137 | ExecuteEveryMonth: Month 138 | ExecuteEveryWeek: Week 139 | ExecuteEveryYear: Year 140 | FIRST_EXECUTION: 'First Execution' 141 | NEXT_RUN_DATE: 'Next run date' 142 | ScheduleTabTitle: Schedule 143 | db_ExecuteEvery: 'Execute every' 144 | db_ExecuteFree: 'Execute free' 145 | db_ExecuteInterval: 'Execute interval' 146 | db_FirstExecution: 'First execution' 147 | has_one_ScheduledJob: 'Scheduled job' 148 | Symbiote\QueuedJobs\Jobs\CleanupJob: 149 | Title: 'Clean up old jobs from the database' 150 | Symbiote\QueuedJobs\Jobs\DeleteObjectJob: 151 | DELETE_JOB: 'Delete node' 152 | DELETE_OBJ2: 'Delete {title}' 153 | Symbiote\QueuedJobs\Jobs\GenerateGoogleSitemapJob: 154 | REGENERATE: 'Regenerate Google sitemap .xml file' 155 | Symbiote\QueuedJobs\Jobs\PublishItemsJob: 156 | Title: 'Publish items beneath {title}' 157 | Symbiote\QueuedJobs\Jobs\ScheduledExecutionJob: 158 | Title: 'Scheduled execution for {title}' 159 | Symbiote\QueuedJobs\Services\QueuedJobService: 160 | BROKEN_JOBS: 'Broken job(s)' 161 | BROKEN_JOBS_MSG: 'The following job(s) appear to be broken.' 162 | JOB_EXCEPT: 'Job caused exception {message} in {file} at line {line}' 163 | JOB_PAUSED: 'Job paused at {time}' 164 | JOB_STALLED: 'Job stalled after {attempts} attempts - please check' 165 | MEMORY_RELEASE: 'Job releasing memory and waiting ({used} used)' 166 | STALLED_JOB: 'Stalled job' 167 | STALLED_JOB_MSG: 'A job named {name} (#{id}) appears to have stalled. It has been paused, please login to check it' 168 | STALLED_JOB_RESTART_MSG: 'A job named {name} (#{id}) appears to have stalled. It will be stopped and restarted, please login to make sure it has continued' 169 | TIME_LIMIT: 'Queue has passed time limit and will restart before continuing' 170 | Symbiote\QueuedJobs\Tasks\CheckJobHealthTask: 171 | Description: 'A task used to check the health of jobs that are "running". Pass a specific queue as the "queue" parameter or otherwise the "Queued" queue will be checked' 172 | Symbiote\QueuedJobs\Tasks\CreateQueuedJobTask: 173 | Description: 'A task used to create a queued job. Pass the queued job class name as the "name" parameter, pass an optional "start" parameter (parseable by strtotime) to set a start time for the job.' 174 | Symbiote\QueuedJobs\Tasks\ProcessJobQueueTask: 175 | Description: 'Used via a cron job to execute queued jobs that need to be run.' 176 | -------------------------------------------------------------------------------- /src/Controllers/QueuedJobsAdmin.php: -------------------------------------------------------------------------------- 1 | 31 | * @license BSD http://silverstripe.org/bsd-license/ 32 | */ 33 | class QueuedJobsAdmin extends ModelAdmin 34 | { 35 | /** 36 | * @var string 37 | */ 38 | private static $url_segment = 'queuedjobs'; 39 | 40 | /** 41 | * @var string 42 | */ 43 | private static $menu_title = 'Jobs'; 44 | 45 | /** 46 | * @var string 47 | */ 48 | private static $menu_icon_class = 'font-icon-checklist'; 49 | 50 | /** 51 | * @var array 52 | */ 53 | private static $managed_models = [ 54 | QueuedJobDescriptor::class 55 | ]; 56 | 57 | /** 58 | * @var array 59 | */ 60 | private static $dependencies = [ 61 | 'jobQueue' => '%$' . QueuedJobService::class, 62 | ]; 63 | 64 | /** 65 | * @var array 66 | */ 67 | private static $allowed_actions = [ 68 | 'EditForm' 69 | ]; 70 | 71 | /** 72 | * European date format 73 | * @var string 74 | */ 75 | private static $date_format_european = 'dd/MM/yyyy'; 76 | 77 | /** 78 | * @var QueuedJobService 79 | */ 80 | public $jobQueue; 81 | 82 | /** 83 | * @config The number of seconds to include jobs that have finished 84 | * default: 7200 (2 hours), examples: 3600(1h), 86400(1d) 85 | */ 86 | private static $max_finished_jobs_age = 7200; 87 | 88 | /** 89 | * @param int $id 90 | * @param FieldList $fields 91 | * @return Form 92 | */ 93 | public function getEditForm($id = null, $fields = null) 94 | { 95 | $form = parent::getEditForm($id, $fields); 96 | 97 | $filter = $this->jobQueue->getJobListFilter(null, static::config()->max_finished_jobs_age); 98 | 99 | $list = QueuedJobDescriptor::get()->where($filter)->sort('Created', 'DESC'); 100 | 101 | $gridFieldConfig = GridFieldConfig_RecordEditor::create() 102 | ->addComponent(GridFieldQueuedJobExecute::create('execute')) 103 | ->addComponent(GridFieldQueuedJobExecute::create('pause', function ($record) { 104 | return $record->JobStatus == QueuedJob::STATUS_WAIT || $record->JobStatus == QueuedJob::STATUS_RUN; 105 | })) 106 | ->addComponent(GridFieldQueuedJobExecute::create('resume', function ($record) { 107 | return $record->JobStatus == QueuedJob::STATUS_PAUSED || $record->JobStatus == QueuedJob::STATUS_BROKEN; 108 | })) 109 | ->removeComponentsByType([ 110 | GridFieldAddNewButton::class, 111 | GridFieldPageCount::class, 112 | GridFieldToolbarHeader::class, 113 | ]); 114 | 115 | // Set messages to HTML display format 116 | $formatting = array( 117 | 'Messages' => function ($val, $obj) { 118 | return "
$obj->Messages
"; 119 | }, 120 | ); 121 | $gridFieldConfig->getComponentByType(GridFieldDataColumns::class) 122 | ->setFieldFormatting($formatting); 123 | 124 | // Replace gridfield 125 | /** @skipUpgrade */ 126 | $grid = GridField::create( 127 | 'QueuedJobDescriptor', 128 | '', 129 | $list, 130 | $gridFieldConfig 131 | ); 132 | $grid->setForm($form); 133 | /** @skipUpgrade */ 134 | $form->Fields()->replaceField($this->sanitiseClassName(QueuedJobDescriptor::class), $grid); 135 | 136 | if (QueuedJobDescriptor::singleton()->canCreate()) { 137 | $types = ClassInfo::subclassesFor(AbstractQueuedJob::class); 138 | $types = array_combine($types ?? [], $types ?? []); 139 | foreach ($types as $class) { 140 | $reflection = new ReflectionClass($class); 141 | if (!$reflection->isInstantiable()) { 142 | unset($types[$class]); 143 | } 144 | } 145 | $jobType = DropdownField::create( 146 | 'JobType', 147 | _t(__CLASS__ . '.CREATE_JOB_TYPE', 'Create job of type'), 148 | $types 149 | ); 150 | $jobType->setEmptyString('(select job to create)'); 151 | $form->Fields()->push($jobType); 152 | 153 | $jobParams = TextareaField::create( 154 | 'JobParams', 155 | _t(__CLASS__ . '.JOB_TYPE_PARAMS', 'Constructor parameters for job creation (one per line)') 156 | ); 157 | $form->Fields()->push($jobParams); 158 | 159 | $form->Fields()->push( 160 | $dt = DatetimeField::create('JobStart', _t(__CLASS__ . '.START_JOB_TIME', 'Start job at')) 161 | ); 162 | 163 | $actions = $form->Actions(); 164 | $actions->push( 165 | FormAction::create('createjob', _t(__CLASS__ . '.CREATE_NEW_JOB', 'Create new job')) 166 | ->addExtraClass('btn btn-primary') 167 | ); 168 | } 169 | 170 | $this->extend('updateEditForm', $form); 171 | 172 | return $form; 173 | } 174 | 175 | /** 176 | * @return string 177 | */ 178 | public function Tools() 179 | { 180 | return ''; 181 | } 182 | 183 | /** 184 | * @param array $data 185 | * @param Form $form 186 | * @return HTTPResponse 187 | */ 188 | public function createjob($data, Form $form) 189 | { 190 | if (QueuedJobDescriptor::singleton()->canCreate()) { 191 | $jobType = isset($data['JobType']) ? $data['JobType'] : ''; 192 | $params = isset($data['JobParams']) ? preg_split('/\R/', trim($data['JobParams'])) : []; 193 | 194 | if (isset($data['JobStart'])) { 195 | $time = is_array($data['JobStart']) ? implode(' ', $data['JobStart']) : $data['JobStart']; 196 | } else { 197 | $time = null; 198 | } 199 | 200 | // If the user has select the European date format as their setting then replace '/' with '-' in the 201 | // date string so PHP treats the date as this format. 202 | if (Security::getCurrentUser()->DateFormat == QueuedJobsAdmin::$date_format_european) { 203 | $time = str_replace('/', '-', $time ?? ''); 204 | } 205 | 206 | if ($jobType && class_exists($jobType ?? '') && is_subclass_of($jobType, QueuedJob::class)) { 207 | $jobClass = new ReflectionClass($jobType); 208 | $job = $jobClass->newInstanceArgs($params); 209 | if ($this->jobQueue->queueJob($job, $time)) { 210 | $form->sessionMessage( 211 | _t(__CLASS__ . '.QueuedJobSuccess', 'Successfully queued job'), 212 | ValidationResult::TYPE_GOOD 213 | ); 214 | } 215 | } 216 | } 217 | return $this->redirectBack(); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/Jobs/GenerateGoogleSitemapJob.php: -------------------------------------------------------------------------------- 1 | pagesToProcess = DB::query('SELECT ID FROM "SiteTree_Live" WHERE "ShowInSearch"=1')->column(); 41 | $this->currentStep = 0; 42 | $this->totalSteps = count($this->pagesToProcess ?? []); 43 | } 44 | 45 | /** 46 | * Sitemap job is going to run for a while... 47 | * 48 | * @return int 49 | */ 50 | public function getJobType() 51 | { 52 | return QueuedJob::QUEUED; 53 | } 54 | 55 | /** 56 | * @return string 57 | */ 58 | public function getTitle() 59 | { 60 | return _t(__CLASS__ . '.REGENERATE', 'Regenerate Google sitemap .xml file'); 61 | } 62 | 63 | /** 64 | * Return a signature for this queued job 65 | * 66 | * For the generate sitemap job, we only ever want one instance running, so just use the class name 67 | * 68 | * @return string 69 | */ 70 | public function getSignature() 71 | { 72 | return md5(get_class($this)); 73 | } 74 | 75 | /** 76 | * Note that this is duplicated for backwards compatibility purposes... 77 | */ 78 | public function setup() 79 | { 80 | parent::setup(); 81 | Environment::increaseTimeLimitTo(); 82 | 83 | $restart = $this->currentStep == 0; 84 | if (!$this->tempFile || !file_exists($this->tempFile ?? '')) { 85 | $tmpfile = tempnam(TempFolder::getTempFolder(BASE_PATH) ?? '', 'sitemap'); 86 | if (file_exists($tmpfile ?? '')) { 87 | $this->tempFile = $tmpfile; 88 | } 89 | $restart = true; 90 | } 91 | 92 | if ($restart) { 93 | $this->pagesToProcess = DB::query('SELECT ID FROM SiteTree_Live WHERE ShowInSearch=1')->column(); 94 | } 95 | } 96 | 97 | /** 98 | * On any restart, make sure to check that our temporary file is being created still. 99 | */ 100 | public function prepareForRestart() 101 | { 102 | parent::prepareForRestart(); 103 | // if the file we've been building is missing, lets fix it up 104 | if (!$this->tempFile || !file_exists($this->tempFile ?? '')) { 105 | $tmpfile = tempnam(TempFolder::getTempFolder(BASE_PATH) ?? '', 'sitemap'); 106 | if (file_exists($tmpfile ?? '')) { 107 | $this->tempFile = $tmpfile; 108 | } 109 | $this->currentStep = 0; 110 | $this->pagesToProcess = DB::query('SELECT ID FROM SiteTree_Live WHERE ShowInSearch=1')->column(); 111 | } 112 | } 113 | 114 | public function process() 115 | { 116 | if (!$this->tempFile) { 117 | throw new Exception("Temporary sitemap file has not been set"); 118 | } 119 | 120 | if (!file_exists($this->tempFile ?? '')) { 121 | throw new Exception("Temporary file $this->tempFile has been deleted!"); 122 | } 123 | 124 | $remainingChildren = $this->pagesToProcess; 125 | 126 | // if there's no more, we're done! 127 | if (!count($remainingChildren ?? [])) { 128 | $this->completeJob(); 129 | $this->isComplete = true; 130 | return; 131 | } 132 | 133 | // lets process our first item - note that we take it off the list of things left to do 134 | $ID = array_shift($remainingChildren); 135 | 136 | // get the page 137 | $page = Versioned::get_by_stage(Page::class, Versioned::LIVE, '"SiteTree_Live"."ID" = ' . $ID); 138 | 139 | if (!$page || !$page->Count()) { 140 | $this->addMessage("Page ID #$ID could not be found, skipping"); 141 | } else { 142 | $page = $page->First(); 143 | } 144 | 145 | if ($page && $page instanceof Page && !($page instanceof ErrorPage)) { 146 | if ($page->canView() && (!isset($page->Priority) || $page->Priority > 0)) { 147 | /** @var DBDatetime $created */ 148 | $created = $page->dbObject('Created'); 149 | $now = DBDatetime::create(); 150 | $now->setValue(DBDatetime::now()->Rfc2822()); 151 | $versions = $page->Version; 152 | $timediff = $now->format('U') - $created->format('U'); 153 | 154 | // Check how many revisions have been made over the lifetime of the 155 | // Page for a rough estimate of it's changing frequency. 156 | $period = $timediff / ($versions + 1); 157 | 158 | if ($period > 60 * 60 * 24 * 365) { // > 1 year 159 | $page->ChangeFreq = 'yearly'; 160 | } elseif ($period > 60 * 60 * 24 * 30) { // > ~1 month 161 | $page->ChangeFreq = 'monthly'; 162 | } elseif ($period > 60 * 60 * 24 * 7) { // > 1 week 163 | $page->ChangeFreq = 'weekly'; 164 | } elseif ($period > 60 * 60 * 24) { // > 1 day 165 | $page->ChangeFreq = 'daily'; 166 | } elseif ($period > 60 * 60) { // > 1 hour 167 | $page->ChangeFreq = 'hourly'; 168 | } else { // < 1 hour 169 | $page->ChangeFreq = 'always'; 170 | } 171 | 172 | // do the generation of the file in a temporary location 173 | $content = $page->renderWith('SitemapEntry'); 174 | 175 | $fp = fopen($this->tempFile ?? '', "a"); 176 | if (!$fp) { 177 | throw new Exception("Could not open $this->tempFile for writing"); 178 | } 179 | fputs($fp, $content ?? '', strlen($content ?? '')); 180 | fclose($fp); 181 | } 182 | } 183 | 184 | // and now we store the new list of remaining children 185 | $this->pagesToProcess = $remainingChildren; 186 | $this->currentStep++; 187 | 188 | if (!count($remainingChildren ?? [])) { 189 | $this->completeJob(); 190 | $this->isComplete = true; 191 | return; 192 | } 193 | } 194 | 195 | /** 196 | * Outputs the completed file to the site's webroot 197 | */ 198 | protected function completeJob() 199 | { 200 | $content = '' . 201 | ''; 202 | $content .= file_get_contents($this->tempFile ?? ''); 203 | $content .= ''; 204 | 205 | $sitemap = Director::baseFolder() . '/sitemap.xml'; 206 | 207 | file_put_contents($sitemap ?? '', $content); 208 | 209 | if (file_exists($this->tempFile ?? '')) { 210 | unlink($this->tempFile ?? ''); 211 | } 212 | 213 | $nextgeneration = Injector::inst()->create(GenerateGoogleSitemapJob::class); 214 | QueuedJobService::singleton()->queueJob( 215 | $nextgeneration, 216 | DBDatetime::create()->setValue( 217 | DBDatetime::now()->getTimestamp() + $this->config()->get('regenerate_time') 218 | )->Rfc2822() 219 | ); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/Services/AbstractQueuedJob.php: -------------------------------------------------------------------------------- 1 | 20 | * @license BSD http://silverstripe.org/bsd-license/ 21 | * @skipUpgrade 22 | */ 23 | abstract class AbstractQueuedJob implements QueuedJob, UserContextInterface 24 | { 25 | /** 26 | * @var stdClass 27 | */ 28 | protected $jobData; 29 | 30 | /** 31 | * @var array 32 | */ 33 | protected $messages = array(); 34 | 35 | /** 36 | * @var int 37 | */ 38 | protected $totalSteps = 0; 39 | 40 | /** 41 | * @var int 42 | */ 43 | protected $currentStep = 0; 44 | 45 | /** 46 | * @var boolean 47 | */ 48 | protected $isComplete = false; 49 | 50 | /** 51 | * Extensions can have a construct but don't have too. 52 | * Without a construct, it's impossible to create a job in the CMS 53 | * @var array params 54 | */ 55 | public function __construct($params = array()) 56 | { 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | abstract public function getTitle(); 63 | 64 | /** 65 | * Sets a data object for persisting by adding its id and type to the serialised vars 66 | * 67 | * @param DataObject $object 68 | * @param string $name A name to give it, if you want to store more than one 69 | */ 70 | protected function setObject(DataObject $object, $name = 'Object') 71 | { 72 | $this->{$name . 'ID'} = $object->ID; 73 | $this->{$name . 'Type'} = $object->ClassName; 74 | } 75 | 76 | /** 77 | * @param string $name 78 | * @return DataObject|null 79 | */ 80 | protected function getObject($name = 'Object') 81 | { 82 | $id = $this->{$name . 'ID'}; 83 | $type = $this->{$name . 'Type'}; 84 | if ($id) { 85 | return DataObject::get($type)->setUseCache(true)->byID($id); 86 | } 87 | } 88 | 89 | /** 90 | * Return a signature for this queued job 91 | * 92 | * @return string 93 | */ 94 | public function getSignature() 95 | { 96 | return md5(get_class($this) . serialize($this->jobData)); 97 | } 98 | 99 | /** 100 | * Generate a somewhat random signature 101 | * 102 | * useful if you're want to make sure something is always added 103 | * 104 | * @return string 105 | */ 106 | protected function randomSignature() 107 | { 108 | return md5(get_class($this) . DBDatetime::now()->getTimestamp() . mt_rand(0, 100000)); 109 | } 110 | 111 | /** 112 | * By default jobs should just go into the default processing queue 113 | * 114 | * @return string 115 | */ 116 | public function getJobType() 117 | { 118 | return QueuedJob::QUEUED; 119 | } 120 | 121 | public function getRunAsMemberID() 122 | { 123 | return null; 124 | } 125 | 126 | /** 127 | * Performs setup tasks the first time this job is run. 128 | * 129 | * This is only executed once for every job. If you want to run something on every job restart, use the 130 | * {@link prepareForRestart} method. 131 | */ 132 | public function setup() 133 | { 134 | $this->loadCustomConfig(); 135 | } 136 | 137 | /** 138 | * Run when an already setup job is being restarted. 139 | */ 140 | public function prepareForRestart() 141 | { 142 | $this->loadCustomConfig(); 143 | } 144 | 145 | /** 146 | * Do some processing yourself! 147 | */ 148 | abstract public function process(); 149 | 150 | /** 151 | * Method for determining whether the job is finished - you may override it if there's 152 | * more to it than just this 153 | */ 154 | public function jobFinished() 155 | { 156 | return $this->isComplete; 157 | } 158 | 159 | /** 160 | * Called when the job is determined to be 'complete' 161 | */ 162 | public function afterComplete() 163 | { 164 | } 165 | 166 | /** 167 | * @return stdClass 168 | */ 169 | public function getJobData() 170 | { 171 | // okay, we NEED to store the subsite ID if there's one available 172 | if (!$this->SubsiteID && class_exists(SubsiteState::class)) { 173 | $this->SubsiteID = SubsiteState::singleton()->getSubsiteId(); 174 | } 175 | 176 | $data = new stdClass(); 177 | $data->totalSteps = $this->totalSteps; 178 | $data->currentStep = $this->currentStep; 179 | $data->isComplete = $this->isComplete; 180 | $data->jobData = $this->jobData; 181 | $data->messages = $this->messages; 182 | 183 | return $data; 184 | } 185 | 186 | /** 187 | * @param int $totalSteps 188 | * @param int $currentStep 189 | * @param boolean $isComplete 190 | * @param stdClass $jobData 191 | * @param array $messages 192 | */ 193 | public function setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages) 194 | { 195 | $this->totalSteps = $totalSteps; 196 | $this->currentStep = $currentStep; 197 | $this->isComplete = $isComplete; 198 | $this->jobData = $jobData; 199 | $this->messages = $messages; 200 | } 201 | 202 | /** 203 | * Gets custom config settings to use when running the job. 204 | * 205 | * @return array|null 206 | */ 207 | public function getCustomConfig() 208 | { 209 | return $this->CustomConfig; 210 | } 211 | 212 | /** 213 | * Sets custom config settings to use when the job is run. 214 | * 215 | * @param array $config 216 | */ 217 | public function setCustomConfig(array $config) 218 | { 219 | $this->CustomConfig = $config; 220 | } 221 | 222 | /** 223 | * Sets custom configuration settings from the job data. 224 | */ 225 | private function loadCustomConfig() 226 | { 227 | $custom = $this->getCustomConfig(); 228 | 229 | if (!is_array($custom)) { 230 | return; 231 | } 232 | 233 | foreach ($custom as $class => $settings) { 234 | foreach ($settings as $setting => $value) { 235 | Config::modify()->set($class, $setting, $value); 236 | } 237 | } 238 | } 239 | 240 | /** 241 | * @param string $message 242 | * @param string $severity 243 | */ 244 | public function addMessage($message, $severity = 'INFO') 245 | { 246 | $severity = strtoupper($severity ?? ''); 247 | $this->messages[] = '[' . DBDatetime::now()->Rfc2822() . "][$severity] $message"; 248 | } 249 | 250 | /** 251 | * Convenience methods for setting and getting job data 252 | * 253 | * @param mixed $name 254 | * @param mixed $value 255 | */ 256 | public function __set($name, $value) 257 | { 258 | if (!$this->jobData) { 259 | $this->jobData = new stdClass(); 260 | } 261 | $this->jobData->$name = $value; 262 | } 263 | 264 | /** 265 | * Retrieve some job data 266 | * 267 | * @param mixed $name 268 | * @return mixed 269 | */ 270 | public function __get($name) 271 | { 272 | return isset($this->jobData->$name) ? $this->jobData->$name : null; 273 | } 274 | 275 | /** 276 | * Resolves a queue name to one of the queue constants. 277 | * If $queue is already the value of one of the constants, it will be returned. 278 | * If the queue is unknown, `null` will be returned. 279 | */ 280 | public static function getQueue(string|int $queue): ?string 281 | { 282 | switch (strtolower($queue)) { 283 | case 'immediate': 284 | $queue = QueuedJob::IMMEDIATE; 285 | break; 286 | case 'queued': 287 | $queue = QueuedJob::QUEUED; 288 | break; 289 | case 'large': 290 | $queue = QueuedJob::LARGE; 291 | break; 292 | default: 293 | $queue = (string) $queue; 294 | $queues = [QueuedJob::IMMEDIATE, QueuedJob::QUEUED, QueuedJob::LARGE]; 295 | if (!ctype_digit($queue) || !in_array($queue, $queues)) { 296 | return null; 297 | } 298 | } 299 | return $queue; 300 | } 301 | } 302 | --------------------------------------------------------------------------------