Skip to content

Cron Job Framework

Flow ID: SY-01 Module(s): job Complexity: Medium Last Updated: 2026-05-27

Business Context

The job framework is a queue-based scheduling system that runs background tasks. Jobs implement the JobCommand interface and are configured in application/config/jobs.php with cron expressions. The system uses a database queue (job_schedule table), supports retries, timeouts, job locking, and 10 named queues for organizational separation.

API Reference

REST Endpoints (Modern Layer)

The cron job framework exposes no REST endpoints. All job execution is triggered by system cron calling php cli.php job_manager; there is no HTTP surface for job control.

Legacy Admin Routes

The cron job framework has no legacy admin routes. Queue management and job status monitoring are performed via CLI and direct inspection of the job_schedule table; no web-facing admin controller exists for this module.

Code Flow

Job Interface (ecommercen/core/JobCommand.php):

php
interface JobCommand {
    public function executeCommand(array $options): void;
    public static function getOptions(): array;
}

Execution path: php cli.php job/{JobName} (legacy) or php cli.php job_manager (queue-based).

Queue-based flow:

  1. AdvCreateJobSchedule reads cron expressions → generates job_schedule rows for today
  2. AdvJobManager spawns Symfony Process per enabled queue
  3. AdvJobQueueManager picks next ready job → executes via subprocess → updates status
  4. AdvClearJobSchedule purges old records per keepLogPeriod

Data Model

job_schedule

database/initial/initial.sql:661-677 (base schema) + database/migrations/20250324124000_add_retry_count_to_job_schedule.php (retry_count column and KEY retry_count index)

sql
CREATE TABLE `job_schedule` (
    `id`            int(11)      NOT NULL AUTO_INCREMENT,
    `queue`         varchar(255) NOT NULL,
    `job`           varchar(100) NOT NULL,
    `start_date`    datetime DEFAULT NULL,
    `end_date`      datetime DEFAULT NULL,
    `run_at`        datetime     NOT NULL,
    `job_status`    int(11)      NOT NULL,
    `pid`           int(11)  DEFAULT NULL,
    `max_retries`   int(11)  DEFAULT NULL,
    `grace_time`    int(11)  DEFAULT NULL,
    `job_arguments` text     DEFAULT NULL,
    `retry_count`   int(11)  NOT NULL DEFAULT 0,
    PRIMARY KEY (`id`),
    KEY `job` (`job`),
    KEY `job_status` (`job_status`),
    KEY `queue` (`queue`),
    KEY `retry_count` (`retry_count`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;

retry_count column and index added by database/migrations/20250324124000_add_retry_count_to_job_schedule.php:13.

ColumnTypePurpose
idint(11) AUTO_INCREMENTPrimary key
queuevarchar(255)Queue name (e.g., core, tracking)
jobvarchar(100)Fully-qualified job class name
start_datedatetimeTimestamp set when the subprocess begins
end_datedatetimeTimestamp set when the subprocess completes
run_atdatetimeScheduled execution time; rows ordered ASC by this column (ecommercen/job/models/AdvJobsModel.php, getNextJob())
job_statusint(11)State machine: NOT_STARTEDRUNNINGJOB_FINISHED / JOB_FAILED (ecommercen/job/controllers/AdvJobQueueManager.php, executeJob())
pidint(11)PID of the executing Symfony Process subprocess
max_retriesint(11)Configured maximum retry count (copied from retryTimes config at schedule creation) (ecommercen/job/libraries/AdvCreateJobSchedule.php:84)
grace_timeint(11)Per-row subprocess kill timeout in seconds
job_argumentstextOptions passed to executeCommand()
retry_countint(11) NOT NULL DEFAULT 0Mutable counter of retry attempts made; incremented by AdvJobQueueManager::trackRetryJob() (database/migrations/20250324124000_add_retry_count_to_job_schedule.php:13)

Configuration

File: application/config/jobs.php

10 queues: core, personalization, tracking, import, giftCards, erp, public_marketplace, shopflix_marketplace, reporting, fileManager.

Each queue has: enabled, keepLogPeriod (DateInterval), hasScheduler, lockJobs, commands[].

Each command: command (class name), schedule (cron expr), graceTime (timeout seconds), retryTimes, options (passed to executeCommand()).

Job implementations: 70+ JobCommand classes in ecommercen/job/libraries/.

Queue status (10 queues configured):

QueueStatusPurpose
coreEnabledPrimary job queue -- order processing, emails, cache, backups
trackingEnabledPrice tracking, delivery status polling
giftCardsEnabledGift card order processing and delivery
reportingEnabledAnalytics export (Databox)
fileManagerEnabledBulk imports (products, images)
importDisabledEmpty — reserved for CSV product import
personalizationDisabledCustomer audience/tagging segmentation
erpDisabledERP sync (Farmacon, Europharmacy)
public_marketplaceDisabledPublic marketplace order sync
shopflix_marketplaceDisabledShopflix marketplace handling

Client Extension Points

Individual job libraries can be overridden at the client application layer by providing a same-named file under application/modules/job/libraries/. The framework will load the client file in preference to the core ecommercen/job/libraries/ counterpart.

Known override path:

  • application/modules/job/libraries/ClearExpiredSessions.php — client-level override for ecommercen/job/libraries/AdvClearExpiredSessions.php. Placing a class here allows the client to substitute custom session GC logic without modifying the core job library.

Adding new jobs (Business Rule #6): New job classes implementing JobCommand can be registered entirely through config — add the class under the appropriate queue's commands[] array in application/config/jobs.php. No framework code changes are required. Custom queues can also be declared in jobs.php for client-specific processing isolation.

Domain Layer

Modern Domain

No modern domain layer exists for the cron framework. The scheduler and all job classes live entirely in the legacy layer.

REST Layer

No REST layer exists for the cron framework. Job execution is triggered by system cron via CLI (php cli.php job_manager); there is no HTTP surface for job control.

Legacy Layer

Order & Fulfillment:

  • AdvCancelIncompleteOrders — Cancels pending orders exceeding payment timeout
  • AdvGetOrdersTransferStatus — Polls courier APIs for delivery tracking (15 carriers); modern port available as Advisable\Domains\Transporter\Jobs\GetOrdersTransferStatus (toggle-activate, #274)
  • AdvSendEmailBasedOnSentStatus — Email on invoiced status
  • AdvSendEmailAndSMSBasedOnSentStatus — Email + SMS when shipped
  • AdvSendEmailAndSMSBasedOnFromStoreStatus — Notify store-based deliveries
  • AdvSendEmailForDeliveryDelay — Alert delayed deliveries
  • AdvSendOrderReminderMail — Order reminder emails
  • AdvEmailPaypalCanceledAndIsPaidToAdmin — Suspicious PayPal state reports

Customer & Marketing:

  • AdvAddCustomersToAudience — Auto-segment customers into audiences
  • AdvAddCustomersToSpecificAudience — Specific audience targeting
  • AdvAddRegisteredTagToCustomers — Tag registered customers
  • AdvAddTagsToCustomers — Personalization tags
  • AdvSanitizeGuestCustomers — Auto-delete guest data per policy
  • AdvSendMailToCustomersForReview — Review request emails
  • AdvSentMailToCustomerBirthday — Birthday greeting emails
  • AdvSentMailToCustomerRemainingPoints — Points reminder emails
  • AdvSendEmailForWaitingList — Stock availability notifications
  • AdvClearOldEmails — Purge old message history

Loyalty:

  • AdvAddPointsToCustomerDeliver — Award points for delivered orders
  • AdvAddPointsToCustomerFromStore — Award points for store purchases

Product & Catalog:

  • Advisable\Domains\Product\PriceTracking\Jobs\TrackPrices — Record price history
  • AdvInsertProductsFromFile — Bulk product import
  • AdvUpdateProductsFromFileByBarcode — Update by barcode CSV
  • AdvUpdateProductsFromFileByProductCode — Update by SKU CSV
  • AdvUploadImagesFromZipFile — Bulk image import
  • AdvUpdateTempOrderBasketTable — Maintain cart analytics table
  • AdvSolrIndex — Index products to Solr

Payment:

  • AdvPayByBankGetStatus — Check PayByBank payment status
  • AdvCancelPendingGiftCards (ecommercen/gift_cards/jobs/) — Cancel expired gift card orders
  • AdvSendGiftCardsToEmails (ecommercen/gift_cards/jobs/) — Email gift card codes
  • AdvSendGiftCardsToPhones (ecommercen/gift_cards/jobs/) — SMS gift card codes

ERP & Marketplace:

  • AdvFarmakonPostOrders / AdvFarmakonSyncProducts — Farmacon ERP sync
  • AdvPostOrdersEuropharmacy / AdvSyncProductsEuropharmacy / AdvSyncOrdersStatusEuropharmacy — Europharmacy ERP
  • AdvGetPublicMarketplaceAwaitingOrders / AdvSetPublicOrdersSent / AdvSetTrackingPublicMarketplace / AdvUpdatePublicOrdersStatus / AdvUpdatePublic2PSupplier — Public marketplace
  • AdvHandleShopflixOrders — Shopflix marketplace
  • AdvRemoveOldSkroutzOrders — Cleanup expired Skroutz orders
  • AdvReportingToDatabox — Analytics export

Infrastructure:

  • AdvBackUpDataBase / AdvCleanBackUps — Database backup lifecycle

  • AdvClearSiteCache / AdvClearCacheOnLimit — Cache management

  • AdvGenerateSitemaps — XML sitemap generation (chunked, 500 URLs/file)

  • AdvCheckCDNCertificate — CDN SSL cert monitoring

  • AdvCheckVideoStatusStream / AdvDeleteVideoToStream / AdvUploadVideoToStream — Video streaming

  • AdvGetBulkerSmsStatus — SMS delivery status

  • AdvClearUsersAudit — Purge audit logs

  • AdvClearExpiredSessions — Deterministic session garbage collection via find -delete. Runs daily at 03:00 (0 3 * * *). Configured with graceTime: 300 (5 minutes) and retryTimes: 0 (application/config/jobs.php:34). The legacy CLI shim execute() delegates to executeCommand([]) (ecommercen/job/libraries/AdvClearExpiredSessions.php:79-82). ClearExpiredSessions::getOptions() returns [], registered as commandOptions (application/config/jobs.php:209). File: ecommercen/job/libraries/AdvClearExpiredSessions.php, client override: application/modules/job/libraries/ClearExpiredSessions.php.

    Execution flow (ecommercen/job/libraries/AdvClearExpiredSessions.php:15-72):

    1. Windows guard (:17-20): logs 'Session GC skipped: not supported on Windows host' and returns. find -delete is not available on Windows; production runs Linux/Docker only.
    2. Driver short-circuit (:22-26): if sess_driver is not 'files' (e.g. redis, database), logs 'Session GC skipped: driver handles its own expiration' and returns. Non-file drivers manage their own TTL.
    3. Config reads (:28-30): reads sess_save_path, sess_cookie_name, and sess_expiration via config_item().
    4. Path canonicalization (:36-41): realpath($savePath) collapses the literal .. in the default config value (FCPATH . '../sessions'), resolves symlinks, and returns false for non-existent paths. Logs an error and aborts if resolution fails or the result is not a directory.
    5. Remaining guards (:42-49): empty sess_cookie_name → error + abort; sess_expiration <= 0 → error + abort.
    6. find -delete (:55-64): invokes find <savePath> -maxdepth 1 -type f -name '<cookie_name>*' -mmin +<minutes> -delete via exec(). Both savePath and the glob pattern are wrapped in escapeshellarg(). sess_expiration (seconds) is converted to minutes with ceil() and a 1-minute floor. Stderr is captured via 2>&1; a non-zero exit code triggers an error log with the captured output.
    7. Success log (:71): "Session GC completed: path=… prefix=… maxlifetime=…s".

    Why find -delete instead of session_gc(): PHP's native files save_handler (used by session_gc()) scans for the sess_* prefix, but CI3 writes session files as <cookie_name><sid> — e.g. ci_session1a2b.... Using session_gc() from CLI targeted the wrong files and silently deleted nothing in production. find -delete with the configured sess_cookie_name as the glob prefix correctly targets the actual CI3 session files (#210).

  • AdvAddProductsToAdvisableAi — AI feed sync

  • AdvCreateJobSchedule / AdvClearJobSchedule — Job framework self-management

  • GenerateOpenApiJson — OpenAPI spec generation

  • RemoveExpiredRefreshTokens — Auth token cleanup

Business Rules

  1. Job locking -- lockJobs: true prevents parallel execution per queue. A running job blocks the queue until it finishes or its maximum runtime is exceeded. (application/config/jobs.php, per-queue lockJobs setting)
  2. Grace time = timeout -- Subprocess killed after graceTime seconds (default 60). Maximum runtime calculated as graceTime * (retryTimes + 1). (ecommercen/job/controllers/AdvJobQueueManager.php, executeJob())
  3. Retry on failure -- retryTimes attempts before marking JOB_FAILED. Between retries, 3-second sleep backoff. (ecommercen/job/controllers/AdvJobQueueManager.php, executeJob())
  4. Cron expressions -- Standard 5-field format, parsed by Cron\CronExpression library. (ecommercen/job/libraries/AdvCreateJobSchedule.php)
  5. Queue isolation -- Each queue runs as separate subprocess via Symfony Process. (ecommercen/job/controllers/AdvJobManager.php, index())
  6. Client customization -- Add jobs to jobs.php config; no code changes needed for scheduling. Custom queues can be added for client-specific processing.
  7. DB reconnect on long runs -- AdvJobsModel::update() calls $this->db->close(); $this->db->initialize() before updates to handle MySQL "gone away" errors. (ecommercen/job/models/AdvJobsModel.php)
  8. Schedule regeneration -- If no NOT_STARTED jobs exist for a queue on invocation, the scheduler regenerates the schedule from config. (ecommercen/job/controllers/AdvJobQueueManager.php, initializeJobs())
  9. Run-at ordering -- Jobs are picked in run_at ASC order; earliest scheduled job runs first. (ecommercen/job/models/AdvJobsModel.php, getNextJob())

Known Issues & Security Gaps

  1. session_gc() return value not checked for failureResolved: the job no longer calls session_gc(). The rewrite (#210) replaced the PHP session GC path with find -delete, which is prefix-aware and targets CI3's actual session files. The exec() return is checked via exit code; non-zero logs an error with captured stderr.

  2. retries column name is misleading — stores a limit, not a counterResolved: column renamed to max_retries in migration 20260416093953_rename_retries_to_max_retries.php (#109).

Tests

No dedicated unit or integration test files exist for the cron job framework classes (AdvJobManager, AdvJobQueueManager, AdvCreateJobSchedule, AdvClearJobSchedule). Testing the queue manager requires a Symfony Process integration harness that is not currently present in the test suite.

AdvClearExpiredSessions is an exception: tests/Legacy/Job/AdvClearExpiredSessionsTest.php (7 cases) covers the Windows skip, driver short-circuit, all four config validation paths (empty/missing path, empty cookie_name, non-positive expiration), and the prefix-aware happy path (asserts expired <cookie_name>* files are deleted while fresh files and prefix-mismatched files are retained). POSIX-only tests skip on Windows.

Platform-wide PHPUnit coverage (670 tests across tests/):

SuiteCount
Unit440
Integration221
Legacy9
Total670

All 18 DDD domains have test coverage; the job scheduler and individual job libraries do not.

System Flows (Jobs)

System Infrastructure

Integration Flows

Wiki Guide: Job Manager Guide -- operational guide for the job scheduling system