# 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/7')) 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