iUsed Refurbisher Portal¶
iUsed Refurbisher Portal is an internal application with some key features:
- Supplier and Refurbisher management
- Offer management from (external) suppliers and intercompany offers
- Product price and stock management
- Product data synchronization to Shopware instances
- Order processing to suppliers as drop shipped orders
- Order processing for CITg intercompany orders
- Integrations with CITg Odoo (via Alumio)
- Deduplication of webshop PDP for SEO purposes
- ... and more!
Documentation¶
-
Test Documentation
Overview of all test cases for the iUsed Portal, including status per module and known bugs.
Purpose¶
The purpose of this application is to simplify and accelerate website content management for both customers and internal users. It supports automated content imports and editing flows, significantly reducing the time required for repetitive tasks that are otherwise time-consuming within PrestaShop.
Infrastructure¶
Horizon Worker¶
Laravel Horizon is a critical background process for the IRP. It runs as a daemon and processes a wide range of background jobs (product synchronization, order processing, offer imports, and more) through a Redis-backed queue.
Critical process
If the Horizon daemon is down, no queued jobs are processed. This directly impacts product syncs, order processing, and other time-sensitive background work.
The Horizon daemon is managed from Ploi and runs as a supervisor process.
Known issue: Horizon fails to start after server updates¶
Recurring problem
After a periodic server update (e.g. unattended package upgrades or a reboot), the Horizon daemon sometimes fails to start and supervisor reports the process as FATAL.
Cause: Redis is often restarted around the same time as the server update. If Redis is not yet available when supervisor starts the Horizon worker, the worker crashes immediately. Because this happens repeatedly within supervisor's default retry window, supervisor gives up and marks the process FATAL instead of retrying further. A process in the FATAL state is not automatically retried by supervisor — it requires a manual (or scripted) start action to recover.
To prevent this, two measures have been implemented.
Mitigation 1 – Supervisor retry configuration¶
The supervisor configuration file for the IRP Horizon worker daemon was manually updated to give Redis more time to come back up, and to allow more start attempts before giving up:
startsecs=5
startretries=5
startsecs requires the process to stay up for 5 seconds before it's considered successfully started, and startretries allows up to 5 start attempts before supervisor marks the process FATAL. This gives Redis a realistic window to finish restarting before Horizon permanently gives up.
Mitigation 2 – Automated recovery script¶
As a safety net for cases where the process still ends up in FATAL (for example if Redis takes longer than the retry window), a recovery script runs every minute via root cron. It checks all supervisor-managed processes and restarts only the ones in the FATAL state, using supervisorctl start (a FATAL process requires an explicit start, not restart, to be cleared).
Script: /usr/local/sbin/supervisor-recover.sh
#!/usr/bin/env bash
set -uo pipefail
SUPERVISORCTL="/usr/bin/supervisorctl"
LOG_TAG="supervisor-recover"
# Prevent concurrent runs
exec 9>/run/lock/supervisor-recover.lock
flock -n 9 || exit 0
# Check that supervisord is reachable
pid=$("$SUPERVISORCTL" pid 2>&1)
if ! [[ "$pid" =~ ^[0-9]+$ ]]; then
logger -t "$LOG_TAG" "ERROR: supervisord unreachable: $pid"
exit 1
fi
# Fetch all process statuses.
# Note: status can return a non-zero exit code when a process is FATAL.
status=$("$SUPERVISORCTL" status 2>&1)
# Only select processes with status FATAL
mapfile -t processes < <(
printf '%s\n' "$status" |
awk '$2 == "FATAL" {print $1}'
)
# Restart every process found
for process in "${processes[@]}"; do
logger -t "$LOG_TAG" "FATAL detected: $process"
if "$SUPERVISORCTL" start "$process"; then
logger -t "$LOG_TAG" "Start executed: $process"
else
logger -t "$LOG_TAG" "ERROR: start failed: $process"
fi
done
The script also supports supervisor processes within groups and multiple workers. The flock prevents two runs from overlapping.
Installation:
sudo chmod 750 /usr/local/sbin/supervisor-recover.sh
sudo crontab -e
Add the following line to root's crontab:
* * * * * /usr/local/sbin/supervisor-recover.sh
Verifying and logging:
# Run manually
sudo /usr/local/sbin/supervisor-recover.sh
# View log entries
journalctl -t supervisor-recover -n 50
Important notes
RUNNING,STARTING,BACKOFF, and intentionally stopped (STOPPED) processes are ignored — onlyFATALis acted on.- The script does not restart the full
supervisorservice, only the individualFATALprocess(es). - If Redis still isn't available when the script runs, the process can end up
FATALagain. The next cron run (within a minute) will retry. - A successful
startcommand does not guarantee the application stays healthy. If problems persist, also check the application logs.
This cron-based recovery is a safety net on top of Mitigation 1 — supervisor's own startretries handles the first recovery attempts, and the cron script catches any case that still slips into FATAL.
Tools¶
Sentry¶
Sentry is used for error tracking and performance monitoring in the IRP. It captures unhandled exceptions and provides detailed stack traces to help debug issues in production.
ClickUp
This integration was set up as part of task IUS-3389.
Project DSN:
https://8c6bc10c774bee4c2ccd57cbe3db6a45@sentry.flooris.dev/13
Install guide
1. Install the package
composer require sentry/sentry-laravel
2. Enable exception capturing in bootstrap/app.php
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Sentry\Laravel\Integration;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions) {
Integration::handles($exceptions);
})->create();
3. Publish the Sentry config and set the DSN
php artisan sentry:publish --dsn=https://8c6bc10c774bee4c2ccd57cbe3db6a45@sentry.flooris.dev/13
This creates config/sentry.php and adds the DSN to your .env:
SENTRY_LARAVEL_DSN=https://8c6bc10c774bee4c2ccd57cbe3db6a45@sentry.flooris.dev/13
Stack trace arguments
To receive stack trace arguments in your errors, set zend.exception_ignore_args: Off in your php.ini.
4. Verify the configuration
php artisan sentry:test
Dependencies¶
Key dependencies of this application are defined in the composer.json. The main packages to be aware of are:
-
Shopware – Enterprise e-commerce platform used for managing product catalogs, orders, and customer data. The Refurbisher Portal synchronizes product data to multiple Shopware instances (Refurbished Direct and Econocom).
-
Ergonode – Product Information Management (PIM) system that centralizes and manages product attributes, descriptions, and enriched product data across the platform. Note: Will be replaced by PrestaShop in the future.
-
External Refurbishers – Third-party supplier integrations that enable automated offer management, product imports, and order processing from external refurbishment partners through standardized connectors.