Background
A light's state is its operational status independent of raw metrics: whether an operator has locked it to a color, plus the timestamps of its green/yellow/red transitions. Because several processes share one data store, transitions must be first-writer-wins: when a light trips, exactly one process should "win" the transition and send the notification, even though many processes observe the failure at once. The Redis store gets this from an atomic set-if-absent; this ticket reproduces the same guarantee with portable ActiveRecord.
Each backend implements the domain state-store port (snapshot, set-state, transition-to-color, clear) by duck typing. The Redis and Memory state stores are the reference implementations to mirror. The epic describes the schema and how a light's id is derived.
Goal
Implement the ActiveRecord State store conforming to the domain state-store port, with the same first-writer-wins transition semantics Redis provides.
What to do
- Add the
stoplight_states migration to the engine (columns and unique index per the epic schema).
- Implement snapshot, set-state, and clear as single-row read, upsert, and delete.
- Implement each
transition_to_color so its return value reports whether the caller was the first writer: ensure the row exists without pre-setting the transition marker column, then run a guarded update whose affected-row count is the boolean. transition_to_red refreshes the scheduling fields (and clears the recovery markers) for every writer while detecting the first writer on the breach marker, matching Redis field for field.
- Timestamps come from the injected clock.
- Add the store's operations to the benchmark harness.
Acceptance criteria
References
Background
A light's state is its operational status independent of raw metrics: whether an operator has locked it to a color, plus the timestamps of its green/yellow/red transitions. Because several processes share one data store, transitions must be first-writer-wins: when a light trips, exactly one process should "win" the transition and send the notification, even though many processes observe the failure at once. The Redis store gets this from an atomic set-if-absent; this ticket reproduces the same guarantee with portable ActiveRecord.
Each backend implements the domain state-store port (snapshot, set-state, transition-to-color, clear) by duck typing. The Redis and Memory state stores are the reference implementations to mirror. The epic describes the schema and how a light's id is derived.
Goal
Implement the ActiveRecord State store conforming to the domain state-store port, with the same first-writer-wins transition semantics Redis provides.
What to do
stoplight_statesmigration to the engine (columns and unique index per the epic schema).transition_to_colorso its return value reports whether the caller was the first writer: ensure the row exists without pre-setting the transition marker column, then run a guarded update whose affected-row count is the boolean.transition_to_redrefreshes the scheduling fields (and clears the recovery markers) for every writer while detecting the first writer on the breach marker, matching Redis field for field.Acceptance criteria
transition_to_coloragainst Postgres shows exactly onetrue.transition_to_redrefreshes the scheduling/recovery fields even for a non-first writer, matching the Redis store's observable result.bundle exec steep checkandbundle exec standardrbpass.References
ports/state_store.rbsredis/storage/state.rb,memory/storage/state.rbset_state.rb,transition_to_color.rb