A bench, not an application: enough to run gplanchat/durable-magento inside a real Magento and
watch what it does. A Tier 1 module is not tested against anything smaller than Magento, so this
directory is the harness.
What is in the repository: composer.json and its lock, compose.yaml, the extension pre-flight
script, five probes, and the probe module they drive. Nothing of the distribution — see
.gitignore, which explains why the rule is inverted there.
Mage-OS installs without Adobe credentials, from https://repo.mage-os.org/. The bench is pinned on
mage-os/product-community-edition:2.2.0, compatible with PHP 8.2, which Durable's dependency graph
imposes.
cd magento
bash ./check-php-extensions.shThe script measures; the list it carries is more reliable than a README's. On Debian or Ubuntu,
ext-pdo_mysql is added by sudo apt-get install -y php8.2-mysql.
cd magento
cp .env.example .env
docker compose up -d
composer installThe ports are shifted on purpose, so as not to fight over those of the neighbouring benches:
| service | host |
|---|---|
| MySQL | 33306 |
| OpenSearch | 9201 |
| Temporal | 7234 |
| Temporal UI | 8088 |
| the shop and its admin | 8080 |
Then the installation, if the database is empty:
bin/magento setup:install \
--base-url=http://127.0.0.1:8080/ \
--db-host=127.0.0.1:33306 --db-name=magento --db-user=magento --db-password=magento \
--admin-firstname=Durable --admin-lastname=Ops --admin-email=durable@example.com \
--admin-user=durable --admin-password='<your password>' \
--language=en_US --currency=USD --timezone=UTC --use-rewrites=1 \
--search-engine=opensearch --opensearch-host=127.0.0.1 --opensearch-port=9201 \
--backend-frontname=admin
bin/magento module:enable Gplanchat_DurableModule
bin/magento setup:upgrade
bin/magento cache:flushThe module is called Gplanchat_DurableModule and its Composer package
gplanchat/durable-magento — the two conventions do not cross, and the
module's registration.php explains why.
The bench enables a second one, Gplanchat_DurableProbe, which lives in
app/code. It is the one that carries the demonstration and the probes: the
published package declares no workflow, and its two di.xml arrays are
empty. An integration module has no business making a project carry workflows
that are not its own.
bin/magento module:enable Gplanchat_DurableModule Gplanchat_DurableProbeOn the command line, the shortest path:
bin/magento durable:demo ORD-4242
# 1. durable.demo.charge
# 2. durable.demo.reserve
# 3. durable.demo.notify
# → 'notify:charge:ORD-4242'The three lines are not labels written into the command: they are the names #[AsActivityMethod]
carries on the contract, resolved at the moment the module assembles its engine.
And Magento knows how to call the repository's three other mockups, through Nexus, on a fourth namespace:
MAGENTO_DC_DURABLE__TEMPORAL__DSN='temporal://127.0.0.1:7239?namespace=demo-magento&tls=0' \
bin/magento durable:demo:nexus MAG-1 1200 MUG_BLUE=1OrderNexusWorkflow has the invoice verified by the Symfony mockup, a delivery slot picked by the
Laravel mockup, the stock held by the Sylius mockup, then charges and ships — the charge being
fulfilled by a workflow on the other side, which takes some fifteen seconds. The bench serves no
operation: calling asks nothing of the host, serving would ask for a handler registry and a Nexus
queue, which do not exist here.
It does not run on its own: the seven other processes, the three endpoints and the prerequisites are
in demo/README.md. The cluster of the compose.yaml above does not fit — its
Nexus APIs are disabled.
In the back office — Magento ships its own development server, there is nothing to install:
php -S 127.0.0.1:8080 -t pub/ phpserver/router.phpThen http://127.0.0.1:8080/admin, and System > Durable processes > Process history. The
screen is read-only: what an operator comes there for is to know whether an order went through, not
to restart it by hand — resuming from a browser would bypass the per-execution lock.
An administration account is added by bin/magento admin:user:create. If two-factor authentication
gets in the way locally: bin/magento module:disable Magento_TwoFactorAuth.
Magento reaches only two backends, and that is final: memory and temporal. The SQL bridges
are refused at installation by a conflict in the module's composer.json, because
ResourceConnection is neither a Doctrine DBAL connection nor an Illuminate one.
It is not a backend name that chooses, it is the presence of a DSN in app/etc/env.php, next
to lock and queue:
'durable' => [
'temporal' => [
'dsn' => 'temporal://127.0.0.1:7234?namespace=default&tls=0',
// Durable's search attributes: the `temporal` healthchecks of symfony/compose.yaml and
// magento/compose.yaml register them, and bin/demo-nexus does on the demo namespaces.
'search_attributes' => true,
],
],Without it, the journal lives in the process that writes it and dies with it — the back-office grid is then empty, and that is the right answer: an administration request opens a fresh process. The page says so itself rather than letting one believe in a failure.
With it, the grid reads the cluster, and lists what was started on the cluster:
RuntimeFactory::resumeDispatcher()->dispatchNewWorkflowRun(), which the probe module's order
observer calls, and workflowClient()->startAsync(), which durable:demo:start calls. Each run shows under its own workflow name:
Run | Workflow | Status | Started
d81bfb25-af86-43b9-a310-9d9d34695a30 | durable.demo.place-order | running | 2026-08-28 09:23:45
Two caveats to know. RuntimeFactory::create()->run(), as durable:demo calls it, follows the
DSN (#765): without one it runs in the process and is not durable; with one it starts on the
cluster and waits for the result, so it needs the workers below. What has to survive the process
without anyone waiting is started with workflowClient()->startAsync(). And a started run stays
running until a worker drains the task queue: nothing advances an execution on the cluster but
bin/magento durable:worker --role=journal # answers workflow tasks
bin/magento durable:worker --role=activity # drains activity tasksOne process, one queue, one role — RunWorkerCommand says why a worker is a command and not a
consumer of Magento's queue. demo/run.sh starts the journal one for the Nexus demonstration; the
workflow it advances has no activity, so no activity worker is started there.
tests/ holds the PHPUnit harness that runs the shared SQL conformance cases of
src/Durable/Testing against the Magento-adapter stores, inside a Magento bootstrap (#747,
DUR056). It uses the repository's PHPUnit, so the bench installs nothing more: run composer install
at the repository root once.
Declare the journal's connection on a second database in app/etc/env.php, and remove
durable/temporal/dsn, which excludes resource/durable (the resolver says so when both are set):
'db' => ['connection' => [
'durable' => ['host' => '127.0.0.1:33306', 'dbname' => 'durable_journal', 'username' => 'magento', 'password' => 'magento', 'model' => 'mysql4', 'engine' => 'innodb', 'initStatements' => 'SET NAMES utf8;', 'active' => '1'],
]],
'resource' => ['durable' => ['connection' => 'durable']],The shop's default connection must answer too: createTable() asks it for the SQL version. If the
shop is not installed on the machine running the harness, point default at any reachable database
of the same MySQL server (for example an empty shop database); the harness never writes to it.
Create the database (CREATE DATABASE durable_journal, with the grant for the magento user), then:
cd magento
php ../vendor/bin/phpunit -c phpunit.xml.dist --fail-on-skippedIf you recreate the journal database between runs, clear the bench's var/cache: Magento's DDL cache
keeps saying the tables exist, createTable() is skipped, and the later ALTER fails with
"Table doesn't exist".
Without resource/durable, the run fails with the resolver's message; it never skips. The harness
also refuses a resource/durable that names the shop's default connection, because it truncates
the durable_* tables before each test.
A store ticket adds a subclass of one shared case in tests/ and builds its store on
JournalHarness::adapter(), which returns the journal's adapter with the schema installed and every
table empty:
final class MagentoEventStoreConformanceTest extends EventStoreConformanceTestCase
{
protected function createEventStore(): EventStoreInterface
{
return new MagentoEventStore(JournalHarness::adapter());
}
}The module is read from vendor/gplanchat/durable-magento, a copy: refresh it as described in
"When the module changes and the bench does not follow".
Five scripts, kept because they replay, and a probe module in app/code that carries the workflows
and the queue topic they drive — outside the published package, because a topic whose handler does
nothing but sleep has no business being in it. Each one says in its header how it is called.
| script | what it measures |
|---|---|
probe-lock.php |
is LockManagerInterface shared across processes? |
probe-queue.php |
what does a consumer that dies in the middle of a message leave behind? |
probe-resume.php |
the failure OST003 names: a consumer killed between the charge and the reservation, restarted under the same identifier, must charge once |
probe-order.php |
§5.2: an order that is placed starts a durable execution, on the cluster |
probe-cases.php |
one execution that holds every case the observation screen has to know how to show |
⚠ Measuring on a dirty queue answers beside the point: a consumer takes the oldest candidate,
not yours. probe-queue.php purge before any campaign.
Six host constraints found while building. Each one cost a round of debugging, and none of them shows up where it is committed.
- Magento forbids
finalon any class its container instantiates: it generates anInterceptorthat extends it. The message — "cannot extend final class" — does not say that the keyword is the cause. - Mage-OS audits path repositories.
composer-dependency-version-audit-pluginrefuses a package resolved locally when a more recent one exists on packagist.org. The bench disables it for itself; a consuming project must keep it. - A module in
app/codedoes not autoload itself on this distribution: Mage-OS's rootcomposer.jsondoes not carry thepsr-0: {"": ["app/code/"]}of a classic Magento. Registering a component and autoloading its classes are two distinct mechanisms, and only the first is automatic. - A controller is resolved by convention from the module name, not from autoloading:
Gplanchat_DurableModule+\Controller\Adminhtml\…. The module name and the package's PSR-4 root have to agree. They once did not —Gplanchat_DurableagainstGplanchat\DurableModule\— and that forced a secondpsr-4entry for that one directory; the module is now named after its namespace and has one. The symptom was misleading: route declared, the menu shows, and Magento serves its 404 in the admin chrome — every sign points at the declaration, which is right. - An optional constructor argument is not autowired: Magento takes its default. It has to be
named in
di.xml, otherwise the dependency staysnullwithout a line of error. - Renaming a class that the container instantiates leaves a stale interceptor in
generated/code/, whichsetup:upgrade --keep-generateddoes not remove. Symptom: There are no commands defined in the "durable" namespace.
The path repositories are "symlink": false: editing src/DurableModule changes nothing in
magento/vendor/gplanchat/durable-magento until it has been reinstalled.
composer update gplanchat/durable-magento gplanchat/durable-bridge-temporal
rm -rf generated/code/Gplanchat
bin/magento setup:upgrade
bin/magento cache:flushComposer reads the path package again from the repository's main copy, not from a worktree: a change made in a worktree will only reach the bench once it has been merged.
Class "Magento\Setup\Mvc\Bootstrap\InitParamListener" not found—composer dump-autoloadinmagento/; the overlay declaresMagento\Setup\in itscomposer.json.You do not have the SUPER privilege … CREATE TRIGGER— the overlay starts MySQL with--log-bin-trust-function-creators=1; recreate the service:docker compose up -d --force-recreate magento-db.Could not validate a connection to the OpenSearch—docker compose ps opensearch, and check port9201.
- This repository does not ship a preinstalled Magento distribution.
- The bench is Mage-OS. Whether the module runs unmodified on Adobe's distribution is an open question, which nobody has measured.