Add configurable storage connection for workflow models and migrations#391
Conversation
|
Merged and released as Packagist is already serving the tag, so it can be installed with: composer require durable-workflow/workflow:2.0.0-alpha.184I also applied a small ECS formatter cleanup before merging; the storage connection behavior from the PR is included in the release. |
|
Thank you so much for your quick response! Now we can continue implementing durable workflows in our project. And thank you greatly for your work! |
|
Follow-up: the latest workflow prerelease is now I also verified the published Packagist artifact with a storage-connection migration smoke: Install the latest prerelease with: composer require durable-workflow/workflow:2.0.0-alpha.186 |
Add configurable storage connection for workflow models and migrations
Summary
This adds an optional
workflows.storage.connectionconfig key that routes allworkflow persistence — every Eloquent model under
src/Models/andsrc/V2/Models/, andevery migration under
src/migrations/— to a dedicated database connection. The keydefaults to
null, which resolves to the application's default connection, so existingdeployments are fully backward compatible and see no behavior change.
Motivation
We want workflow state to live in a separate
durable_workflowdatabase, isolated from ourcentral/tenant OLTP databases, across both MariaDB and PostgreSQL. Today the package gives no
supported way to do that:
src/Models/andsrc/V2/Models/extends
Illuminate\Database\Eloquent\Modeldirectly and never overridesgetConnectionName(), so workflow storage always lands on the application's defaultconnection.
ConfiguredV2Models::resolve('*_model', …)mechanism only covers some models. Severaltables are also reached through hardcoded class references (e.g.
new WorkflowMessage(),WorkflowChildCall::,WorkerCompatibilityHeartbeat::) alongside theresolve()path.Overriding a model only on the
resolve()side would leave the hardcoded call sites on thedefault connection — the same table read on two connections (a split-brain). The only
correct place to route the connection is the model class itself.
(
telescope.storage.database.connection), Horizon, Passport, andspatie/laravel-activitylog(activitylog.database_connection) all expose this. A durableworkflow engine — the system of record for critical long-running processes — benefits even
more from isolating its store: independent backup/retention/scaling, multi-driver setups,
and tenant isolation.
What changed
storageblock insrc/config/workflows.php:DW_*→WORKFLOW_*→ default env contract.Workflow\Traits\ResolvesStorageConnection— a small trait that overridesgetConnectionName()to returnconfig('workflows.storage.connection') ?? $this->connection.Applied to all 33 models. Because the routing lives on the model class, both hardcoded and
resolve()-based usages resolve to the same connection — eliminating the split-brain.Workflow\Support\WorkflowMigration— an abstractMigrationsubclass that overridesgetConnection()the same way. All 39 migrations now extend it instead ofIlluminate\Database\Migrations\Migration.migration routing onto a second connection, and a fail-closed contract test that every
package migration extends
WorkflowMigration.Backward compatibility
The key defaults to
null. Withnull,getConnectionName()/getConnection()return themodel's own
$connection(alsonull), i.e. the application's default connection — exactlytoday's behavior. Existing apps are unaffected; no migration or config change is required.
How it works
For migrations the routing relies on Laravel's
Migrator:runMigration()resolves theconnection from
$migration->getConnection()(overriding the--databaseoption), andrunMethod()temporarily sets that connection as the default for the duration ofup()/down(). That is why the bareSchema::create(...)/Schema::dropIfExists(...)calls needno change — they land on the configured connection automatically. The migration repository
rows still record on the run connection, which is standard Laravel behavior (the same as
Telescope).
Testing
Tests\Unit\StorageConnectionTest— withworkflows.storage.connection = 'secondary',asserts
getConnectionName() === 'secondary'forStoredWorkflow(v1),WorkflowRun(v2 resolve-path) andWorkflowMessage(v2 hardcoded-usage); with the keyunset, asserts the null/default fallback.
Tests\Unit\Migrations\StorageConnectionMigrationTest— registers a secondsqliteconnection, points
workflows.storage.connectionat it, runs the migrations, then assertsrepresentative workflow tables (
workflows,workflow_runs,workflow_messages) exist onthe secondary connection and are absent on the default connection.
Tests\Unit\Migrations\MigrationsTest::testEveryPackageMigrationExtendsWorkflowMigrationBase— a fail-closed contract test (matching the existing migration-slate contracts) asserting
every
src/migrations/*.phpextendsWorkflowMigrationand none extends the framework'sbare
Migration. This guards against a futuremake:migrationregenerating a migration onthe default base class and silently bypassing the storage-connection routing.