Skip to content

feat(pathways): configurable state prefix for pathway state - #106

Merged
jbiskur merged 1 commit into
mainfrom
feat/state-prefix
Aug 27, 2026
Merged

feat(pathways): configurable state prefix for pathway state#106
jbiskur merged 1 commit into
mainfrom
feat/state-prefix

Conversation

@jbiskur

@jbiskur jbiskur commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Description

Two deployables that share ONE PostgreSQL connection string contend for a single cluster leader lease. The lease key pathway-cluster-leader is a global constant — not namespaced by pathwayName, data core, or anything else — and the state tables use fixed names.

The loser logs Could not acquire lease, becoming worker and Not starting pump — this instance is not the cluster leader, then never starts its pump. It never recovers while the other holder keeps renewing. Writes still succeed, so the failure is silent at the API layer and only the projection stalls.

Observed in production on 2026-08-26 in flowcore-io/flowcore-container-service, namespace flowcore-public: compute-api and compute-reconciler share a connection string by design. The reconciler took the lease and both compute-api pods stayed pumpless for more than 6 minutes.

Task: ed7ce624-3ce8-459f-adf8-b1b8a686ac47

What changed

Every state library this package owns accepts an optional statePrefix.

State No prefix (default) statePrefix: "compute_api"
PostgresPathwayState pathway_state compute_api_pathway_state
PostgresPathwayCoordinator pathway_leases compute_api_pathway_leases
PostgresPathwayCoordinator pathway_instances compute_api_pathway_instances
Postgres pump state pathway_pump_state compute_api_pathway_pump_state
Leader lease key pathway-cluster-leader compute_api_pathway-cluster-leader

ClusterManager resolves its lease key from leaseKey, then statePrefix, then coordinator.leaseKey, then the default. Setting the prefix on the coordinator alone is enough — one config point.

ClusterManager now also logs the resolved leaseKey on start and on every role change. Two deployables that report the same key are contending. That makes this class of failure visible instead of silent.

Second defect

The task noted the reconciler leader logging Worker WS error against ws://127.0.0.1:9091 every 10 seconds. Same root cause: a shared pathway_instances table made the leader dial the other deployable's pods. Prefixing fixes it. Covered by a test.

Safety

A prefix reaches SQL as part of an identifier, so it is validated against ^[A-Za-z_][A-Za-z0-9_]*$ with a 40 character cap. Injection-shaped input is rejected at construction, not at query time.

Backwards compatibility

The default is no prefix. Existing deployments keep their exact table names and their lease key value. There is no migration. An explicit tableName, leasesTable, instancesTable or leaseKey still overrides everything.

Acceptance

# Requirement Where
1 A state configuration option sets the prefix, default none src/pathways/state-prefix.ts
2 Two prefixes on one database each lead their own cluster and start their own pump tests/cluster-state-prefix.test.ts, tests/postgres-state-prefix.test.ts
3 No prefix keeps current names and key, no migration tests/state-prefix.test.ts, tests/postgres-state-prefix.test.ts
4 The same option covers every other state library pathway state, pump state, leases, instances
5 A test proves two prefixes on one database do not contend tests/postgres-state-prefix.test.ts

Type of Change

  • New feature (non-breaking change)
  • Bug fix (non-breaking change)
  • Documentation update

Testing

  • Tests pass locally — deno test -A, 38 passed, 186 steps, 0 failed
  • Added tests for new functionality — 3 new test files, 22 steps
  • Added tests for edge cases — injection-shaped prefixes, underscore-only, over-length, precedence order
  • PostgreSQL tests pass — tests/postgres-state-prefix.test.ts runs against a live database and drops what it creates

Checklist

  • deno fmt, deno lint, deno check src/mod.ts, deno run -A bin/build-npm.ts all clean
  • JSDoc on every new public API
  • README updated with a new State Prefix section and a TOC entry
  • No breaking changes

Follow-up

Two Usable fragments describe the constant lease key and should be updated after release: Cluster Mode Complete Guide f538652e-fdf5-4a3d-9a77-ddb32201093a and API Reference e2ff5c89-0f4f-4f17-aff6-870e9ac1327e.

🤖 Generated with Claude Code

Two deployables that share one PostgreSQL connection string contended for a
single cluster leader lease. The lease key `pathway-cluster-leader` was a global
constant, not namespaced by pathwayName or anything else, and the state tables
used fixed names. The loser logged `Could not acquire lease, becoming worker`,
never started its pump, and never recovered while the other holder kept
renewing. Writes still succeeded, so the failure was silent at the API layer.

Every state library this package owns now accepts an optional `statePrefix`:

- `PostgresPathwayCoordinator` -> `${p}pathway_leases`, `${p}pathway_instances`
  and a `leaseKey` of `${p}pathway-cluster-leader`
- `createPostgresPumpStateManagerFactory` -> `${p}pathway_pump_state`
- `PostgresPathwayState` -> `${p}pathway_state`
- `PathwayClusterOptions` -> `statePrefix` and an explicit `leaseKey` escape hatch

`ClusterManager` resolves its lease key from `leaseKey`, then `statePrefix`, then
`coordinator.leaseKey`, then the default, so the prefix is set in one place only.
It also logs the resolved key on start and on every role change, which makes this
class of contention visible instead of silent.

The default is no prefix. Existing deployments keep their exact table names and
lease key value with no migration.

A shared `pathway_instances` table also made the leader dial another deployable's
pods, which produced the repeating `Worker WS error` log. Prefixing fixes that too.

Prefixes reach SQL as part of an identifier, so they are validated against
`^[A-Za-z_][A-Za-z0-9_]*$` with a 40 character cap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jbiskur
jbiskur force-pushed the feat/state-prefix branch from 4146c73 to 58e6c03 Compare August 26, 2026 16:37
@jbiskur

jbiskur commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

CI note

Push events on this repository are not creating GitHub Actions runs. Two pushes to feat/state-prefix produced zero workflow runs, and neither push appears in the repository event feed, although the commits landed on the remote. The last push-triggered run on this repository is from 2026-08-16.

Actions itself is healthy: enabled: true, allowed_actions: all at both repository and organisation level, and the Test workflow is active.

To get this PR verified I added workflow_dispatch: to .github/workflows/validate.yml and started the workflow manually. That run succeeded in starting, which confirms the problem is push-event delivery, not Actions.

Manual run: https://github.com/flowcore-io/flowcore-pathways/actions/runs/32989521917

The workflow_dispatch trigger is worth keeping. It is additive and it gives a manual re-run path. Remove it in review if you prefer.

@jbiskur

jbiskur commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

CI is green on the PR head commit 58e6c0309f43809e97d49db17f638304b6fbe1bf.

Step Result
Validate NPM build success
Lint success
Format success
Test success
Test PostgreSQL success

Run: https://github.com/flowcore-io/flowcore-pathways/actions/runs/32989521917

@jbiskur
jbiskur force-pushed the feat/state-prefix branch from 58e6c03 to 7ba51e2 Compare August 26, 2026 17:48
@jbiskur

jbiskur commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Correction to the CI note above, and cleanup

My earlier comment blamed push-event delivery on this repository. That was wrong.

The real cause was the GitHub Actions incident that started at 2026-08-26 15:11 UTC — critical impact, Actions major_outage, a database primary failure followed by upstream Vitess problems. GitHub throttled inbound Actions work while recovering. The pushes on this branch landed inside that window, so no run was queued at the time. Git, Webhooks and the API stayed operational throughout, which is why the commits themselves pushed normally.

The queued runs arrived once GitHub drained the backlog. Every one of them passed:

Created (UTC) Event Commit Result
16:53 push 7ba51e2 success
16:47 push 58e6c03 success
16:46 push 4146c73 success
16:38 workflow_dispatch 58e6c03 success

Since the normal push path works, the workflow_dispatch: trigger was not needed. I dropped that commit and force-pushed. This PR now contains one commit and touches no CI configuration. .github/workflows/validate.yml is unchanged from main.

@jbiskur
jbiskur merged commit dd0dc5d into main Aug 27, 2026
3 checks passed
@jbiskur
jbiskur deleted the feat/state-prefix branch August 27, 2026 07:58
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