Reliable Cron Jobs for PrestaShop 9: Locks, Retries and Recovery
A scheduler answers when to start a job. It does not guarantee that the previous run has finished, that the process will survive or that an external action will happen only once. Reliable PrestaShop module jobs need their own execution contract.
Imagine a scheduled fulfillment export that runs every five minutes. One run slows down, the next starts and both see the same pending order. Even if each process is individually correct, the combined result can be two exports. A lock can prevent some overlaps, but it cannot resolve every crash or uncertain remote outcome.
This article focuses on execution reliability for existing PrestaShop 9 modules. The designs are application recommendations, not a claim that core provides a universal job ledger or queue for every module.
For a scheduled catalog feed, combine execution controls with a resumable import design so a retry resumes defined work rather than restarting an ambiguous batch.
Define one logical job before choosing a lock
A job identity should describe the work being serialized. Include shop scope and any business partition that permits independent execution. For example, exporting one shop’s orders may be independent of another shop’s export, while two runs for the same shop should coordinate.
Distinguish a run identifier from a logical operation key. The run ID identifies one process attempt. The operation key identifies the business effect, such as transferring a particular order revision to a particular destination. A new run should not automatically create a new identity for the same business effect.
Make scope explicit in command arguments or the queued message. A background process has no reliable reason to inherit the shop selected in an employee’s browser. Validate the supplied shop against the module’s configured associations and permitted operation.
Define completion before implementing it. Does a successful run mean every eligible record was processed, one bounded batch was processed or an external service accepted a request? Those are different outcomes and should not share an ambiguous success flag.
Use exclusion appropriate to the deployment topology
For one host with cooperating processes and a suitable local filesystem, a file lock can provide simple mutual exclusion. It does not automatically coordinate multiple containers with independent filesystems or several application servers.
Symfony’s Lock component provides different stores and ownership semantics. Choose a backend that matches the deployment and understand its expiry behavior before relying on it. Symfony 6.4 Lock component.
This small PHP example is a local lock acquisition helper, not a complete PrestaShop command. The caller must keep the returned handle open until the protected work finishes. The directory must be pre-created outside the web root and writable only by the worker account.
<?php
function acquireLocalJobLock(string $directory, int $shopId)
{
if ($shopId < 1 || !is_dir($directory)) {
throw new InvalidArgumentException('Invalid lock scope or directory');
}
$path = rtrim($directory, DIRECTORY_SEPARATOR)
. DIRECTORY_SEPARATOR . 'export-shop-' . $shopId . '.lock';
$handle = fopen($path, 'c');
if ($handle === false) {
throw new RuntimeException('Cannot open lock file');
}
if (!flock($handle, LOCK_EX | LOCK_NB)) {
fclose($handle);
return null;
}
return $handle;
}
Release the lock in a finally block with flock($handle, LOCK_UN) and fclose($handle) when a handle was acquired. Do not delete the lock file on release: replacing its inode can allow different processes to lock different files under the same pathname.
For a distributed lease, expiry adds another problem. A paused worker can resume after its lease expires while a new worker already owns the job. Renewal and, where needed, fencing checks at the write boundary are part of the design. A long timeout alone is not a proof of exclusive ownership.
Make the business effect safe to repeat
Mutual exclusion prevents concurrent entry within its scope. Idempotency prevents a repeated logical operation from applying an unintended second effect. You generally need both.
Suppose a worker sends an order to an external service and then crashes before marking it complete locally. On restart, the local record still looks pending. The correct response is not to assume the external request failed.
Use a stable operation key accepted by the remote service when its API supports one. Otherwise, reconcile using a durable external reference before retrying. Store enough result information to distinguish accepted, rejected and unknown outcomes.
For purely local effects, enforce a unique business key in the database and keep the effect and its completion record in the same transaction where the storage and domain operation permit it. A “check then insert” sequence without a uniqueness constraint can race under concurrent workers.
Symfony’s Messenger documentation explains why messages can be delivered again after processing and why handlers need repeat-safe behavior. That principle applies even when your trigger is cron rather than a message transport. Idempotent handler guidance.
Do not label a job idempotent because running it twice in a quiet development environment happened to produce the same result. Test the failure boundary between effect and acknowledgement.
Classify failures before retrying
Separate transient failures from invalid work. A temporary connection failure may merit a bounded retry. A missing required identifier or rejected business rule usually needs correction or quarantine. Retrying invalid input every minute creates noise without progress.
Use a delay policy with a maximum attempt count and an upper bound. Add jitter when many workers could otherwise retry together. Respect the external service’s documented retry and rate-limit instructions.
Record the next attempt time and the last error category. Avoid a single free-text error field as the only machine-readable state. Operators need to distinguish waiting for retry from permanently blocked work.
For uncertain outcomes, use a reconciliation state rather than immediately returning the item to the ordinary pending queue. That makes the risk of duplicate effects explicit.
A manual retry action should preserve the original operation identity. If it creates a new business key, it can defeat the protection designed for automatic retries.
Commit bounded progress and expose it to operators
Process a bounded batch or stop after a deliberate work budget. Long jobs should not depend on one process surviving until every record is complete.
A useful ledger includes run ID, logical scope, start time, heartbeat, last committed progress, completed count, failed count and final state. The exact table or storage is module-owned; do not assume a core schema with these fields exists.
Advance progress only after the corresponding work is durably complete. If the checkpoint moves first and the process dies before the effect, the next run can skip unfinished work. If the effect happens first and the checkpoint is lost, idempotency or reconciliation must make replay safe.
Distinguish “skipped because another run holds the lock” from “completed the workload.” Both may be non-error scheduler outcomes, but they mean different things to an operator investigating missing progress.
A stale heartbeat should trigger investigation, not immediate destructive recovery. The worker may be paused or an external operation may still be in flight. Establish ownership and outcome before resetting state.
Prefer a controlled execution interface
A CLI command is often a clearer scheduling boundary because it can return an exit code, receive explicit arguments and avoid a browser request’s time limits. It still needs the correct application bootstrap, service account, working directory and configuration.
If HTTP scheduling is required, authenticate it, restrict its purpose and keep the request bounded. Do not place a reusable secret in a public URL that will be copied into access logs. Do not let arbitrary request parameters select a command or filesystem path.
A short HTTP trigger can enqueue or claim work and return while a supervised worker performs it, if that architecture is deliberately implemented. Merely returning a response early does not guarantee that background work will continue reliably.
Use the module’s existing supported command registration rather than creating an ad hoc PHP entry point that bypasses its normal validation and services. The concern here is reliable execution, not another generic command tutorial.
For operational ownership, define who reviews quarantined work and how quickly it must be addressed. Automatic retries cannot correct a permanently invalid mapping or an expired business credential. A visible queue with no responsible reviewer is still a failure mode, even when its retry logic is technically correct.
Drill the failures you expect production to survive
Run two attempts concurrently and verify the chosen exclusion scope. Kill a worker after a committed batch, restart it and verify that completed work is not repeated incorrectly. Simulate a temporary remote failure and confirm that retries stop or quarantine as designed.
For an external side effect, test a timeout after the remote system may have accepted the request. The recovery path should reconcile the operation rather than blindly repeat it.
Use two shops with distinct destinations or configuration. Confirm that a worker for one shop cannot consume or acknowledge the other’s records. Test shutdown at the point where the checkpoint is about to advance.
Finally, ask an operator to identify what happened using only the status and logs you expose. If they cannot tell whether the job is running, waiting, failed or uncertain, the reliability design is incomplete even if the code handles the happy path.
A dependable cron job is a small recovery system. Explicit identity, suitable exclusion, repeat-safe effects and honest progress states allow the scheduler to trigger work repeatedly without making repeated execution dangerous.
For the underlying platform context, see our PrestaShop 9 module development guide and apply the version-specific checks above to the installation you are evaluating.
When selecting an extension, browse PrestaSuite’s PrestaShop 9 modules and check the documented version support and integration requirements against your own test results.