diff --git a/INSTALL.md b/INSTALL.md index 24b7a9503..a75ad5296 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -77,6 +77,68 @@ Start the containers using the following command docker-compose up ``` +## Background Job Scheduling + +OSPOS includes a background job queue (Office → Job Queue) for long-running tasks such as large CSV imports. It supports three trigger modes, configured on the Job Queue → Settings tab: + +- **Web** (default) — no setup required. Jobs are processed via a request hook after page loads, using `fastcgi_finish_request()` where available. Works out of the box on shared hosting, VPS, and Docker. +- **Auto** — a cron entry (Linux/Mac) or Task Scheduler task (Windows) triggers processing on a fixed interval. Recommended for VPS/dedicated servers with cron access. +- **Manual** — an admin clicks a "Process All Jobs" button on the Job Queue → Utilities tab. Always available regardless of mode, useful for debugging. + +### `auto` mode: Linux/Mac cron + +Add the following entry to your crontab (`crontab -e`), adjusting the path to your OSPOS install: + +``` +* * * * * cd /path/to/ospos && php spark tasks:run >> /dev/null 2>&1 +``` + +### `auto` mode: Windows Task Scheduler + +1. Open Task Scheduler and create a new task. +2. Trigger: `Daily`, check `Repeat task every: 5 minutes` (the fastest interval the GUI allows), `for a duration of: Indefinitely`, no expiration. + - Don't use a `One time` trigger with a fixed repeat duration (e.g. `1 day`) — it stops repeating once that duration elapses instead of running forever. +3. Action: start a program. + - Program/script: `php.exe` (full path, e.g. `C:\php\php.exe`) + - Arguments: `spark tasks:run` + - Start in: **must be the OSPOS project root — the directory containing the `spark` file** (e.g. `C:\laragon\www\opensourcepos`, or `C:\wamp64\www\opensourcepos\public\..`). This is NOT your PHP installation directory. If `Start in` is wrong or blank, `spark` fails immediately with `Could not open input file: spark` and the window closes before you can read it. + - Alternatively, avoid relying on `Start in` altogether by giving the full path to `spark` directly in Arguments: `Arguments: C:\laragon\www\opensourcepos\spark tasks:run`. +4. On the **General** tab, select **"Run whether user is logged on or not"**. Without this, the task runs in your interactive session and briefly flashes a console window every time it fires. + - You'll be prompted for your account password to save the task. If it's rejected (common with Microsoft accounts using Windows Hello — fingerprint/PIN/face login won't work here), check **"Do not store password"** instead. This limits the task to local computer resources only, which is sufficient for `spark tasks:run`. +5. Save the task. It will invoke the scheduler every 5 minutes — less frequent than the once-per-minute cron example above, so jobs are processed in 5-minute batches instead. +6. Before trusting the scheduled task, verify it manually: open `cmd.exe`, `cd` to the same `Start in` directory, and run the same `Program/script` + `Arguments`. You should see `Running Tasks...` then `Completed Running Tasks`, and a new heartbeat line in `writable/logs/`. + +### `manual` mode + +No setup is required. Set Mode to Manual on the Job Queue → Settings tab, then use the "Process All Jobs" button on the Utilities tab whenever you need jobs processed. + +### Docker considerations + +Docker containers are single-process by default, so cron does not run inside the OSPOS container out of the box — this is why **Web mode is the default** and works with zero extra Docker configuration. + +If you want `auto` mode under Docker, pick one of the following: + +**Option 1: separate worker container (recommended)** + +Add a second service to your `docker-compose.yml` pointing at the same database: + +```yaml +worker: + image: opensourcepos + command: php spark tasks:run + depends_on: + - db + restart: unless-stopped +``` + +**Option 2: supervisor inside the container** + +Add supervisor to the image and configure it to run both the web server and the scheduler. Works, but goes against the one-process-per-container convention — acceptable for simple single-container setups. + +**Option 3: cron sidecar container** + +Run a minimal sidecar container that fires `php spark tasks:run` every minute via cron, sharing the same network and database as the main container. + ## Nginx install using Docker Since OSPOS version `3.3.0` the Docker installation offers a reverse proxy based on Nginx with a Let's Encrypt TLS certificate termination (aka HTTPS connection). diff --git a/app/Models/JobThrottle.php b/app/Models/JobThrottle.php new file mode 100644 index 000000000..032b3caa9 --- /dev/null +++ b/app/Models/JobThrottle.php @@ -0,0 +1,80 @@ +db->table('job_throttles'); + $builder->where('throttle_id', $throttleId); + + return ($builder->get()->getNumRows() >= 1); + } + + /** + * @param array $throttleData + * @param int $throttleId + * @return bool + */ + public function saveValue(array $throttleData, int $throttleId): bool + { + $throttleDataToSave = [ + 'max_count' => $throttleData['max_count'], + 'period' => $throttleData['period'], + 'deleted' => 0 + ]; + + if (!$this->exists($throttleId)) { + $builder = $this->db->table('job_throttles'); + return $builder->insert($throttleDataToSave); + } + + $builder = $this->db->table('job_throttles'); + $builder->where('throttle_id', $throttleId); + + return $builder->update($throttleDataToSave); + } + + /** + * @return ResultInterface + */ + public function getAll(): ResultInterface + { + $builder = $this->db->table('job_throttles'); + $builder->where('deleted', 0); + + return $builder->get(); + } + + /** + * Deletes one throttle + */ + public function delete($throttleId = null, bool $purge = false): bool + { + $builder = $this->db->table('job_throttles'); + $builder->where('throttle_id', $throttleId); + + return $builder->update(['deleted' => 1]); + } +} diff --git a/composer.json b/composer.json index f12384fa4..a21b79c23 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "php": "^8.2", "ext-intl": "*", "codeigniter4/framework": "4.7.4", + "codeigniter4/tasks": "^1.0", "dompdf/dompdf": "^3.1.6", "ezyang/htmlpurifier": "^4.17", "laminas/laminas-escaper": "2.18.0", diff --git a/composer.lock b/composer.lock index 661a34712..cd4fa86b7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "caa6ba9519694ccf54b8a487640e87fd", + "content-hash": "7f9d30d341a97be2979b62b5732ee285", "packages": [ { "name": "codeigniter4/framework", @@ -83,6 +83,183 @@ }, "time": "2026-07-07T09:23:35+00:00" }, + { + "name": "codeigniter4/queue", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/codeigniter4/queue.git", + "reference": "87f736a69a0d64bce0c40bcb1a3f966250f70f03" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/codeigniter4/queue/zipball/87f736a69a0d64bce0c40bcb1a3f966250f70f03", + "reference": "87f736a69a0d64bce0c40bcb1a3f966250f70f03", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "codeigniter4/devkit": "^1.3", + "codeigniter4/framework": "^4.3", + "php-amqplib/php-amqplib": "^3.7", + "phpstan/phpstan-strict-rules": "^2.0", + "predis/predis": "^2.0 || ^3.0" + }, + "suggest": { + "ext-redis": "If you want to use RedisHandler", + "php-amqplib/php-amqplib": "If you want to use RabbitMQHandler", + "predis/predis": "If you want to use PredisHandler" + }, + "type": "library", + "autoload": { + "psr-4": { + "CodeIgniter\\Queue\\": "src" + }, + "exclude-from-classmap": [ + "**/Database/Migrations/**" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "michalsn", + "homepage": "https://github.com/michalsn", + "role": "Developer" + } + ], + "description": "Queues for CodeIgniter 4 framework", + "homepage": "https://github.com/codeigniter4/queue", + "keywords": [ + "codeigniter", + "codeigniter4", + "database", + "predis", + "queue", + "redis" + ], + "support": { + "issues": "https://github.com/codeigniter4/queue/issues", + "source": "https://github.com/codeigniter4/queue/tree/v1.0.1" + }, + "time": "2026-07-23T17:03:38+00:00" + }, + { + "name": "codeigniter4/settings", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/codeigniter4/settings.git", + "reference": "7cbd2e2146e32211cc8194d9c35632631500adad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/codeigniter4/settings/zipball/7cbd2e2146e32211cc8194d9c35632631500adad", + "reference": "7cbd2e2146e32211cc8194d9c35632631500adad", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "codeigniter4/devkit": "^1.3", + "codeigniter4/framework": "^4.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "CodeIgniter\\Settings\\": "src" + }, + "exclude-from-classmap": [ + "**/Database/Migrations/**" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lonnie Ezell", + "email": "lonnieje@gmail.com", + "role": "Developer" + } + ], + "description": "Settings library for CodeIgniter 4", + "homepage": "https://github.com/codeigniter4/settings", + "keywords": [ + "Settings", + "codeigniter", + "codeigniter4" + ], + "support": { + "issues": "https://github.com/codeigniter4/settings/issues", + "source": "https://github.com/codeigniter4/settings/tree/v2.4.0" + }, + "time": "2026-07-30T16:03:46+00:00" + }, + { + "name": "codeigniter4/tasks", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/codeigniter4/tasks.git", + "reference": "30a329a040f86e49d723b7c2eaa64836a703eb55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/codeigniter4/tasks/zipball/30a329a040f86e49d723b7c2eaa64836a703eb55", + "reference": "30a329a040f86e49d723b7c2eaa64836a703eb55", + "shasum": "" + }, + "require": { + "codeigniter4/queue": "^1.0", + "codeigniter4/settings": "^2.0", + "ext-json": "*", + "php": "^8.2" + }, + "require-dev": { + "codeigniter4/devkit": "^1.3", + "codeigniter4/framework": "^4.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "CodeIgniter\\Tasks\\": "src" + }, + "exclude-from-classmap": [ + "**/Database/Migrations/**" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lonnie Ezell", + "email": "lonnieje@gmail.com", + "role": "Developer" + } + ], + "description": "Task Scheduler for CodeIgniter 4", + "homepage": "https://github.com/codeigniter4/tasks", + "keywords": [ + "codeigniter", + "codeigniter4", + "cron", + "task scheduling" + ], + "support": { + "issues": "https://github.com/codeigniter4/tasks/issues", + "source": "https://github.com/codeigniter4/tasks/tree/v1.0.1" + }, + "time": "2026-03-30T05:38:19+00:00" + }, { "name": "dompdf/dompdf", "version": "v3.1.6", @@ -5546,5 +5723,5 @@ "ext-intl": "*" }, "platform-dev": {}, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" }