Skip to content

Add configurable storage connection for workflow models and migrations#391

Open
discovery-ukraine wants to merge 1 commit into
durable-workflow:v2from
discovery-ukraine:feat/configurable-storage-connection
Open

Add configurable storage connection for workflow models and migrations#391
discovery-ukraine wants to merge 1 commit into
durable-workflow:v2from
discovery-ukraine:feat/configurable-storage-connection

Conversation

@discovery-ukraine
Copy link
Copy Markdown

Add configurable storage connection for workflow models and migrations

Summary

This adds an optional workflows.storage.connection config key that routes all
workflow persistence — every Eloquent model under src/Models/ and src/V2/Models/, and
every migration under src/migrations/ — to a dedicated database connection. The key
defaults to null, which resolves to the application's default connection, so existing
deployments are fully backward compatible and see no behavior change.

Motivation

We want workflow state to live in a separate durable_workflow database, isolated from our
central/tenant OLTP databases, across both MariaDB and PostgreSQL. Today the package gives no
supported way to do that:

  • No connection hook on the models. Every model in src/Models/ and src/V2/Models/
    extends Illuminate\Database\Eloquent\Model directly and never overrides
    getConnectionName(), so workflow storage always lands on the application's default
    connection.
  • Consumer-side model swaps cannot route everything, and would split-brain. The
    ConfiguredV2Models::resolve('*_model', …) mechanism only covers some models. Several
    tables are also reached through hardcoded class references (e.g. new WorkflowMessage(),
    WorkflowChildCall::, WorkerCompatibilityHeartbeat::) alongside the resolve() path.
    Overriding a model only on the resolve() side would leave the hardcoded call sites on the
    default connection — the same table read on two connections (a split-brain). The only
    correct place to route the connection is the model class itself.
  • Routing the store is an industry-standard capability. Telescope
    (telescope.storage.database.connection), Horizon, Passport, and
    spatie/laravel-activitylog (activitylog.database_connection) all expose this. A durable
    workflow 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

  • Config key — a new top-level storage block in src/config/workflows.php:
    'storage' => [
        'connection' => Env::dw('DW_STORAGE_CONNECTION', 'WORKFLOW_STORAGE_CONNECTION', null),
    ],
    It uses the existing DW_*WORKFLOW_* → default env contract.
  • Workflow\Traits\ResolvesStorageConnection — a small trait that overrides
    getConnectionName() to return config('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 abstract Migration subclass that overrides
    getConnection() the same way. All 39 migrations now extend it instead of
    Illuminate\Database\Migrations\Migration.
  • Tests — model connection resolution (incl. backward-compatible null fallback),
    migration routing onto a second connection, and a fail-closed contract test that every
    package migration extends WorkflowMigration.
  • Docs — a "Using a dedicated storage connection" section in the README.

Backward compatibility

The key defaults to null. With null, getConnectionName() / getConnection() return the
model's own $connection (also null), i.e. the application's default connection — exactly
today'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 the
connection from $migration->getConnection() (overriding the --database option), and
runMethod() temporarily sets that connection as the default for the duration of up() /
down(). That is why the bare Schema::create(...) / Schema::dropIfExists(...) calls need
no 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 — with workflows.storage.connection = 'secondary',
    asserts getConnectionName() === 'secondary' for StoredWorkflow (v1),
    WorkflowRun (v2 resolve-path) and WorkflowMessage (v2 hardcoded-usage); with the key
    unset, asserts the null/default fallback.
  • Tests\Unit\Migrations\StorageConnectionMigrationTest — registers a second sqlite
    connection, points workflows.storage.connection at it, runs the migrations, then asserts
    representative workflow tables (workflows, workflow_runs, workflow_messages) exist on
    the 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/*.php extends WorkflowMigration and none extends the framework's
    bare Migration. This guards against a future make:migration regenerating a migration on
    the default base class and silently bypassing the storage-connection routing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant