You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The admin dashboard can crash or silently misreport a light's configuration. Its lock/unlock path obtains a light through the definition API (System#register(name)) with no per-light overrides. When the app registered the same light with overrides, the config-consistency digest check sees a conflict and raises Stoplight::Error::ConfigurationError -- every admin lock/unlock click crashes.
When admin runs in a separate process it instead silently registers a defaults-configured light: no crash, wrong config on display (the same happens in v5.x). And because the registry persists only {version, registered_at}, a separate-process admin has no way to learn a light's real configuration at all.
Root cause: the API conflated three verbs: define, look up, and operate, and admin is forced to define when it only needed to operate.
Design overview
The stabilized API gives each verb exactly one path:
Operate: admin-only, through the storage facade. Admin never constructs a Light. lock/unlock only write to the state store, which is keyed by light name, so per-light config values play no role in the action. The digest check disappears from every admin path because admin no longer participates defining lights.
Three supporting decisions make this work end to end:
Matchers become named constants.
tracked_errors / skipped_errors accept any Class or Module with a non-nil .name - exception classes as the common case, named custom matchers overriding === for complex cases. Nameless matchers (procs, Class.new, instances) are rejected at definition time.
Reason: durable config is digested (cross-process, needs reload-stable identity), serialized (registry), and displayed (dashboard) all three need a stable string per matcher. This also fixes a bug (verified locally) where a Zeitwerk reload made the digest see "different" settings and raise an error.
The restriction stops where its reasons stop.
Per-call tracked_errors: / skipped_errors: overrides on Light#run (issue #738, implemented separately) stay open to any === matcher, closures included: they are ephemeral, never digested or persisted, and the call site is the only place dynamic matching is expressible. Registered config is the durable identity of a light -- per-call arguments are ephemeral behavior.
The registry persists real config, display-only.
Registration writes the light's actual settings to the registry, so a separate-process dashboard can show real config. Writes are last-write-wins with config drift reported through error_notifier as a warning -- never a raise, because processes re-register at boot and a raise would turn every rolling deploy into a boot crash. Persisted descriptors are for humans: nothing ever reconstructs them into live objects.
Lock/unlock also gains observability: the operation (color mapping, state write, LockChanged telemetry emission) is extracted into one domain object shared by Light and the admin, so both emit the same event and neither can drift from the other.
Summary
What we'll get when this is completed:
we can display configs in admin
Lights could be reloaded and operate normally
admin can run in the same process with the main application
The admin dashboard can crash or silently misreport a light's configuration. Its
lock/unlockpath obtains a light through the definition API (System#register(name)) with no per-light overrides. When the app registered the same light with overrides, the config-consistency digest check sees a conflict and raisesStoplight::Error::ConfigurationError-- every adminlock/unlockclick crashes.When admin runs in a separate process it instead silently registers a defaults-configured light: no crash, wrong config on display (the same happens in v5.x). And because the registry persists only
{version, registered_at}, a separate-process admin has no way to learn a light's real configuration at all.Root cause: the API conflated three verbs: define, look up, and operate, and admin is forced to define when it only needed to operate.
Design overview
The stabilized API gives each verb exactly one path:
Stoplight.register(name, **settings), withStoplight(name, **settings)kept as define-or-fetch sugar. Idempotent for identical settings,ConfigurationErroron a same-process digest mismatch (PR Add Stoplight.register / Stoplight.light for reusable, faster-looked-up lights #739 shape, unchanged).Stoplight.light(name), a plain cache read that raisesUnregisteredLightErrorfor unknown names (PR Add Stoplight.register / Stoplight.light for reusable, faster-looked-up lights #739 shape, unchanged).Light.lock/unlockonly write to the state store, which is keyed by light name, so per-light config values play no role in the action. The digest check disappears from every admin path because admin no longer participates defining lights.Three supporting decisions make this work end to end:
tracked_errors/skipped_errorsaccept anyClassorModulewith a non-nil.name- exception classes as the common case, named custom matchers overriding===for complex cases. Nameless matchers (procs,Class.new, instances) are rejected at definition time.Reason: durable config is digested (cross-process, needs reload-stable identity), serialized (registry), and displayed (dashboard) all three need a stable string per matcher. This also fixes a bug (verified locally) where a Zeitwerk reload made the digest see "different" settings and raise an error.
Per-call
tracked_errors:/skipped_errors:overrides onLight#run(issue #738, implemented separately) stay open to any===matcher, closures included: they are ephemeral, never digested or persisted, and the call site is the only place dynamic matching is expressible. Registered config is the durable identity of a light -- per-call arguments are ephemeral behavior.Registration writes the light's actual settings to the registry, so a separate-process dashboard can show real config. Writes are last-write-wins with config drift reported through
error_notifieras a warning -- never a raise, because processes re-register at boot and a raise would turn every rolling deploy into a boot crash. Persisted descriptors are for humans: nothing ever reconstructs them into live objects.Lock/unlockalso gains observability: the operation (color mapping, state write,LockChangedtelemetry emission) is extracted into one domain object shared byLightand the admin, so both emit the same event and neither can drift from the other.Summary
What we'll get when this is completed: