Skip to content

Cron Job Framework ​

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

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_STARTED → RUNNING → JOB_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; modern port available as Advisable\Domains\Order\Jobs\GetBulkerSmsStatus (toggle-activate, #155) — see IN-14 SMS for full behaviour detail

  • 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:39). 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:237). 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.

Modern PSR-4 job ports alongside legacy jobs: A legacy Adv* job (classmap CI) may have a modern PSR-4 counterpart living in src/Domains/{Context}/Jobs/ and registered in that context's container.php. In application/config/jobs.php the modern FQCN schedule entry is commented out next to the legacy bare-string entry; un-commenting the modern entry (and commenting out the legacy one) switches the active path with no other changes required. Container registration is mandatory for modern ports: the AdvJob dispatcher resolves via $container->has($jobName) ? $container->get($jobName) : new $jobName(), so a modern job with constructor dependencies fatals on the new fallback if it is not registered in the container. Current examples: Transporter domain GetOrdersTransferStatus (#274), Checkout domain PollPayByBankStatus (#163), Order domain GetBulkerSmsStatus (#155). 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 failure — Resolved: 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 counter — Resolved: 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