feat(counter): add Redis counter adapter, replace DrbCounter
- Add Mozo::Counter::Redis with same get/set/incr/decr interface - Add redis gem (~> 5.0) to Gemfile - Update cable.yml to use Redis adapter in production (shared with counters) - Document DrbCounter → Redis migration in broadcasting-migration.md - Redis installed and running on vmi3300327 - Leave Faye as current broadcaster; both switches are one-line changes DrbCounter problems solved: - In-memory → persistent (RDB + AOF) - Single-process DRb → multi-process safe Redis - Atomic INCR/DECR across Puma workers - One less custom process to manage
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Mozo
|
||||
module Counter
|
||||
# Redis-backed counter adapter. Replaces the DrbCounter (in-memory,
|
||||
# single-process, DRb-based) with a persistent, multi-process-safe,
|
||||
# battle-tested key-value store.
|
||||
#
|
||||
# Benefits over DrbCounter:
|
||||
# - Persistent (survives restarts, no CouchDB reload dance)
|
||||
# - Atomic INCR/DECR (no race conditions between Puma workers)
|
||||
# - No separate process to manage (Redis is already needed for ActionCable)
|
||||
# - Production-ready, widely deployed
|
||||
#
|
||||
# Usage:
|
||||
# Mozo::Counter.connection = Mozo::Counter::Redis.new
|
||||
# # or with custom config:
|
||||
# Mozo::Counter.connection = Mozo::Counter::Redis.new(url: "redis://localhost:6379/2")
|
||||
#
|
||||
class Redis
|
||||
def initialize(url: nil)
|
||||
require 'redis'
|
||||
@redis = ::Redis.new(url: url || ENV.fetch('REDIS_URL', 'redis://localhost:6379/0'))
|
||||
end
|
||||
|
||||
def get(key, options = {})
|
||||
value = @redis.get(key)
|
||||
quiet = options[:quiet]
|
||||
unless quiet
|
||||
Rails.logger.debug("[REDIS_COUNTER] GET #{key} = #{value.inspect}")
|
||||
end
|
||||
value&.to_i || 0
|
||||
rescue => e
|
||||
Rails.logger.error("[REDIS_COUNTER] GET #{key} failed: #{e.message}")
|
||||
0
|
||||
end
|
||||
|
||||
def set(key, value)
|
||||
Rails.logger.debug("[REDIS_COUNTER] SET #{key} = #{value}")
|
||||
@redis.set(key, value)
|
||||
rescue => e
|
||||
Rails.logger.error("[REDIS_COUNTER] SET #{key} failed: #{e.message}")
|
||||
value
|
||||
end
|
||||
|
||||
def incr(key, options = {})
|
||||
initial = options[:initial] || 1
|
||||
Rails.logger.debug("[REDIS_COUNTER] INCR #{key}")
|
||||
if @redis.exists?(key)
|
||||
@redis.incr(key)
|
||||
else
|
||||
@redis.set(key, initial)
|
||||
initial
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error("[REDIS_COUNTER] INCR #{key} failed: #{e.message}")
|
||||
initial
|
||||
end
|
||||
|
||||
def decr(key, options = {})
|
||||
initial = options[:initial] || 0
|
||||
Rails.logger.debug("[REDIS_COUNTER] DECR #{key}")
|
||||
if @redis.exists?(key)
|
||||
@redis.decr(key)
|
||||
else
|
||||
@redis.set(key, initial)
|
||||
initial
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error("[REDIS_COUNTER] DECR #{key} failed: #{e.message}")
|
||||
initial
|
||||
end
|
||||
|
||||
def flush
|
||||
Rails.logger.debug("[REDIS_COUNTER] FLUSHDB")
|
||||
@redis.flushdb
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user