From 1d0aa4d1d900cd60cabb4e65719fb673ea9ef935 Mon Sep 17 00:00:00 2001 From: Bastian Schon Date: Thu, 27 Aug 2026 11:05:33 +0200 Subject: [PATCH] Document that an adapter instance represents one resource Semian::Adapter's contract is that the object it is included in stands for a single resource, and that once that resource is acquired, further access through the same instance is part of the same session: nested calls neither re-enter the circuit breaker nor take a second bulkhead ticket, so a circuit that opens elsewhere cannot interrupt a session that is already in flight and succeeding. Every adapter that ships with the gem has that shape - the mixin goes into a connection object. Nothing said so, and the failure is silent when the requirement is not met: with the instance shared process-wide, calls from other threads are treated as part of whatever session is in flight, so they are not fast-failed when the circuit is open and their failures are not counted. State the contract in Creating Adapters and in Thread Safety, and suggest a per-unit-of-work session object as the adapterized wrapper around a singleton, noting that circuit and bulkhead state is keyed by semian_identifier in a process-wide registry, so short-lived adapter instances still share one circuit. Refs #1045 Co-authored-by: Claude Opus 5 Orchestrated-by: ae Assisted-By: devx/39c7b308-c333-4ffe-8cd7-f642a14a4b7c --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d3e9d1e5..896f0f6bd 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,8 @@ To create a Semian adapter you must implement the following methods: 1. [`include Semian::Adapter`][semian-adapter]. Use the helpers to wrap the resource. This takes care of situations such as monitoring, nested resources, unsupported platforms, creating the Semian resource if it doesn't already - exist and so on. + exist and so on. Include it in an object that represents a single resource, + typically one connection — see [Thread Safety](#thread-safety). 2. `#semian_identifier`. This is responsible for returning a symbol that represents every unique resource, for example `redis_master` or `mysql_shard_1`. This is usually assembled from a `name` attribute on the @@ -214,6 +215,27 @@ Semian's circuit breaker implementation is thread-safe by default as of `v0.7.0`. If you'd like to disable it for performance reasons, pass `thread_safety_disabled: true` to the resource options. +An adapter instance represents one resource. Once that resource has been +acquired, Semian treats further access through the same instance as part of the +same session: nested or overlapping calls do not re-enter the circuit breaker +and do not take a second bulkhead ticket, so a circuit that opens elsewhere in +the process cannot interrupt a session that is already in flight and +succeeding. Every adapter that ships with Semian has this shape — the mixin is +included in a connection object, and the session is that connection's unit of +work. + +This is likely to give unexpected results if the adapter instance is shared +process-wide, such as by a singleton. While any call is in flight, calls made +through the same instance from other threads are treated as part of that +session, so they are not fast-failed when the circuit has opened since that +first acquire, and they cannot contribute failures. Where calls overlap +continuously, that keeps the circuit closed while the resource is failing. +Consider introducing a "session" object as the adapterized wrapper around the +singleton instead, created per unit of work — per request, say. Circuit breaker +and bulkhead state is keyed by `semian_identifier` and held in a process-wide +registry, so short-lived adapter instances that share an identifier all share +one circuit. + Bulkheads should be disabled (pass `bulkhead: false`) in a threaded environment (e.g. Puma or Sidekiq), but can safely be enabled in non-threaded environments (e.g. Resque and Unicorn). As described in this document, circuit breakers alone