💎 ChatSDK Ruby

Concurrency

Per-thread locking, event deduplication, and multi-process safety for concurrent event handling.

ChatSDK is designed to handle concurrent events safely, even across multiple processes. It uses per-thread locking and event deduplication to prevent race conditions.

Background execution

Assuming adapter and state are configured, a plain Ruby worker can use a callable handler_executor to enqueue JSON-safe payloads for another thread to process with dispatch_serialized:

require "json"

dispatch_queue = Queue.new
bot = ChatSDK::Chat.new(
  user_name: "my-bot",
  adapters: {slack: adapter},
  state: state,
  handler_executor: ->(payload) { dispatch_queue << JSON.generate(payload) }
)

worker = Thread.new do
  loop do
    payload = JSON.parse(dispatch_queue.pop)
    bot.dispatch_serialized(payload)
  end
end

This in-memory queue is an example for a single process. Use a durable queue and a persistent state backend shared by web and worker processes when jobs must survive restarts or coordinate deduplication and locks across processes.

Per-Thread Locking

When an event arrives, ChatSDK acquires a lock scoped to the thread (identified by adapter name, channel ID, and thread ID). While the lock is held, other events for the same thread are handled according to the on_lock_conflict policy.

bot = ChatSDK::Chat.new(
  user_name: "my-bot",
  adapters: { slack: slack },
  state: ChatSDK::State::Redis.new,
  on_lock_conflict: :drop  # default
)

Lock Conflict Policies

PolicyBehavior
:dropSilently discard the conflicting event (default)
:forceTake the lock and process the event
ProcCustom logic that returns :force or :drop

Custom Policy Example

bot = ChatSDK::Chat.new(
  user_name: "my-bot",
  adapters: { slack: slack },
  state: state,
  on_lock_conflict: ->(thread_key, event) {
    # Always process actions (button clicks), drop concurrent mentions
    event.type == :action ? :force : :drop
  }
)

Locks have a 30-second TTL to prevent deadlocks if a handler crashes.

Event Deduplication

Platforms sometimes deliver the same event more than once (retries, network issues). ChatSDK deduplicates events by storing their IDs in the state backend with a configurable TTL.

bot = ChatSDK::Chat.new(
  user_name: "my-bot",
  adapters: { slack: slack },
  state: state,
  dedupe_ttl: 600  # Ignore duplicate events within 10 minutes (default)
)

The deduplication key is derived from the event's message ID (for mention/subscribed_message events) or event_id from the raw payload.

Multi-Process Safety

For multi-process deployments (multiple dynos, Puma workers, Sidekiq processes), use a persistent state backend: ChatSDK::State::Redis, ChatSDK::State::Pg, or ChatSDK::State::Mysql. All three provide atomic operations for locks and deduplication records.

state = ChatSDK::State::Redis.new(url: ENV["REDIS_URL"])

bot = ChatSDK::Chat.new(
  user_name: "my-bot",
  adapters: { slack: slack },
  state: state
)

State::Memory is not safe across multiple processes because it stores data in a single Ruby process.

Lock Owner Identity

Lock owners are identified by "#{Process.pid}:#{Thread.current.object_id}". This means:

  • Different processes never share the same owner identity
  • Different Ruby threads within the same process have different identities
  • A process restart clears stale locks automatically via TTL expiration

Thread Safety Within a Process

State::Memory uses a Mutex to protect all reads and writes. Multiple Ruby threads can safely dispatch events concurrently when using the Memory backend.

State::Redis relies on Redis's single-threaded command execution for atomicity. Lock operations use SET NX PX and Lua scripts for safe release.

On this page