Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
143
research/chatwoot/lib/redis/alfred.rb
Normal file
143
research/chatwoot/lib/redis/alfred.rb
Normal file
@@ -0,0 +1,143 @@
|
||||
# refer : https://redis.io/commands
|
||||
|
||||
module Redis::Alfred
|
||||
include Redis::RedisKeys
|
||||
|
||||
class << self
|
||||
# key operations
|
||||
|
||||
# set a value in redis
|
||||
def set(key, value, nx: false, ex: false) # rubocop:disable Naming/MethodParameterName
|
||||
$alfred.with { |conn| conn.set(key, value, nx: nx, ex: ex) }
|
||||
end
|
||||
|
||||
# set a key with expiry period
|
||||
# TODO: Deprecate this method, use set with ex: 1.day instead
|
||||
def setex(key, value, expiry = 1.day)
|
||||
$alfred.with { |conn| conn.setex(key, expiry, value) }
|
||||
end
|
||||
|
||||
def get(key)
|
||||
$alfred.with { |conn| conn.get(key) }
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
$alfred.with { |conn| conn.del(key) }
|
||||
end
|
||||
|
||||
# increment a key by 1. throws error if key value is incompatible
|
||||
# sets key to 0 before operation if key doesn't exist
|
||||
def incr(key)
|
||||
$alfred.with { |conn| conn.incr(key) }
|
||||
end
|
||||
|
||||
def exists?(key)
|
||||
$alfred.with { |conn| conn.exists?(key) }
|
||||
end
|
||||
|
||||
# set expiry on a key in seconds
|
||||
def expire(key, seconds)
|
||||
$alfred.with { |conn| conn.expire(key, seconds) }
|
||||
end
|
||||
|
||||
# scan keys matching a pattern
|
||||
def scan_each(match: nil, count: 100, &)
|
||||
$alfred.with do |conn|
|
||||
conn.scan_each(match: match, count: count, &)
|
||||
end
|
||||
end
|
||||
|
||||
# count keys matching a pattern
|
||||
def keys_count(pattern)
|
||||
count = 0
|
||||
scan_each(match: pattern) { count += 1 }
|
||||
count
|
||||
end
|
||||
|
||||
# list operations
|
||||
|
||||
def llen(key)
|
||||
$alfred.with { |conn| conn.llen(key) }
|
||||
end
|
||||
|
||||
def lrange(key, start_index = 0, end_index = -1)
|
||||
$alfred.with { |conn| conn.lrange(key, start_index, end_index) }
|
||||
end
|
||||
|
||||
def rpop(key)
|
||||
$alfred.with { |conn| conn.rpop(key) }
|
||||
end
|
||||
|
||||
def lpush(key, values)
|
||||
$alfred.with { |conn| conn.lpush(key, values) }
|
||||
end
|
||||
|
||||
def rpoplpush(source, destination)
|
||||
$alfred.with { |conn| conn.rpoplpush(source, destination) }
|
||||
end
|
||||
|
||||
def lrem(key, value, count = 0)
|
||||
$alfred.with { |conn| conn.lrem(key, count, value) }
|
||||
end
|
||||
|
||||
# hash operations
|
||||
|
||||
# add a key value to redis hash
|
||||
def hset(key, field, value)
|
||||
$alfred.with { |conn| conn.hset(key, field, value) }
|
||||
end
|
||||
|
||||
# get value from redis hash
|
||||
def hget(key, field)
|
||||
$alfred.with { |conn| conn.hget(key, field) }
|
||||
end
|
||||
|
||||
# get values of multiple keys from redis hash
|
||||
def hmget(key, fields)
|
||||
$alfred.with { |conn| conn.hmget(key, *fields) }
|
||||
end
|
||||
|
||||
# sorted set operations
|
||||
|
||||
# add score and value for a key
|
||||
# Modern Redis syntax: zadd(key, [[score, member], ...])
|
||||
def zadd(key, score, value = nil)
|
||||
if value.nil? && score.is_a?(Array)
|
||||
# New syntax: score is actually an array of [score, member] pairs
|
||||
$alfred.with { |conn| conn.zadd(key, score) }
|
||||
else
|
||||
# Support old syntax for backward compatibility
|
||||
$alfred.with { |conn| conn.zadd(key, [[score, value]]) }
|
||||
end
|
||||
end
|
||||
|
||||
# get score of a value for key
|
||||
def zscore(key, value)
|
||||
$alfred.with { |conn| conn.zscore(key, value) }
|
||||
end
|
||||
|
||||
# count members in a sorted set with scores within the given range
|
||||
def zcount(key, min_score, max_score)
|
||||
$alfred.with { |conn| conn.zcount(key, min_score, max_score) }
|
||||
end
|
||||
|
||||
# get the number of members in a sorted set
|
||||
def zcard(key)
|
||||
$alfred.with { |conn| conn.zcard(key) }
|
||||
end
|
||||
|
||||
# get values by score
|
||||
def zrangebyscore(key, range_start, range_end, with_scores: false, limit: nil)
|
||||
options = {}
|
||||
options[:with_scores] = with_scores if with_scores
|
||||
options[:limit] = limit if limit
|
||||
$alfred.with { |conn| conn.zrangebyscore(key, range_start, range_end, **options) }
|
||||
end
|
||||
|
||||
# remove values by score
|
||||
# exclusive score is specified by prefixing (
|
||||
def zremrangebyscore(key, range_start, range_end)
|
||||
$alfred.with { |conn| conn.zremrangebyscore(key, range_start, range_end) }
|
||||
end
|
||||
end
|
||||
end
|
||||
49
research/chatwoot/lib/redis/config.rb
Normal file
49
research/chatwoot/lib/redis/config.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
module Redis::Config
|
||||
DEFAULT_SENTINEL_PORT ||= '26379'.freeze
|
||||
class << self
|
||||
def app
|
||||
config
|
||||
end
|
||||
|
||||
def config
|
||||
@config ||= sentinel? ? sentinel_config : base_config
|
||||
end
|
||||
|
||||
def base_config
|
||||
{
|
||||
url: ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379'),
|
||||
password: ENV.fetch('REDIS_PASSWORD', nil).presence,
|
||||
ssl_params: { verify_mode: Chatwoot.redis_ssl_verify_mode },
|
||||
reconnect_attempts: 2,
|
||||
timeout: 1
|
||||
}
|
||||
end
|
||||
|
||||
def sentinel?
|
||||
ENV.fetch('REDIS_SENTINELS', nil).presence
|
||||
end
|
||||
|
||||
def sentinel_url_config(sentinel_url)
|
||||
host, port = sentinel_url.split(':').map(&:strip)
|
||||
sentinel_url_config = { host: host, port: port || DEFAULT_SENTINEL_PORT }
|
||||
password = ENV.fetch('REDIS_SENTINEL_PASSWORD', base_config[:password])
|
||||
sentinel_url_config[:password] = password if password.present?
|
||||
sentinel_url_config
|
||||
end
|
||||
|
||||
def sentinel_config
|
||||
redis_sentinels = ENV.fetch('REDIS_SENTINELS', nil)
|
||||
|
||||
# expected format for REDIS_SENTINELS url string is host1:port1, host2:port2
|
||||
sentinels = redis_sentinels.split(',').map do |sentinel_url|
|
||||
sentinel_url_config(sentinel_url)
|
||||
end
|
||||
|
||||
# over-write redis url as redis://:<your-redis-password>@<master-name>/ when using sentinel
|
||||
# more at https://github.com/redis/redis-rb/issues/531#issuecomment-263501322
|
||||
master = "redis://#{ENV.fetch('REDIS_SENTINEL_MASTER_NAME', 'mymaster')}"
|
||||
|
||||
base_config.merge({ url: master, sentinels: sentinels })
|
||||
end
|
||||
end
|
||||
end
|
||||
63
research/chatwoot/lib/redis/lock_manager.rb
Normal file
63
research/chatwoot/lib/redis/lock_manager.rb
Normal file
@@ -0,0 +1,63 @@
|
||||
# Redis::LockManager provides a simple mechanism to handle distributed locks using Redis.
|
||||
# This class ensures that only one instance of a given operation runs at a given time across all processes/nodes.
|
||||
# It uses the $alfred Redis namespace for all its operations.
|
||||
#
|
||||
# Example Usage:
|
||||
#
|
||||
# lock_manager = Redis::LockManager.new
|
||||
#
|
||||
# if lock_manager.lock("some_key")
|
||||
# # Critical code that should not be run concurrently
|
||||
# lock_manager.unlock("some_key")
|
||||
# end
|
||||
#
|
||||
class Redis::LockManager
|
||||
# Default lock timeout set to 1 second. This means that if the lock isn't released
|
||||
# within 1 second, it will automatically expire.
|
||||
# This helps to avoid deadlocks in case the process holding the lock crashes or fails to release it.
|
||||
LOCK_TIMEOUT = 1.second
|
||||
|
||||
# Attempts to acquire a lock for the given key.
|
||||
#
|
||||
# If the lock is successfully acquired, the method returns true. If the key is
|
||||
# already locked or if any other error occurs, it returns false.
|
||||
#
|
||||
# === Parameters
|
||||
# * +key+ - The key for which the lock is to be acquired.
|
||||
# * +timeout+ - Duration in seconds for which the lock is valid. Defaults to +LOCK_TIMEOUT+.
|
||||
#
|
||||
# === Returns
|
||||
# * +true+ if the lock was successfully acquired.
|
||||
# * +false+ if the lock was not acquired.
|
||||
def lock(key, timeout = LOCK_TIMEOUT)
|
||||
value = Time.now.to_f.to_s
|
||||
# nx: true means set the key only if it does not exist
|
||||
Redis::Alfred.set(key, value, nx: true, ex: timeout) ? true : false
|
||||
end
|
||||
|
||||
# Releases a lock for the given key.
|
||||
#
|
||||
# === Parameters
|
||||
# * +key+ - The key for which the lock is to be released.
|
||||
#
|
||||
# === Returns
|
||||
# * +true+ indicating the lock release operation was initiated.
|
||||
#
|
||||
# Note: If the key wasn't locked, this operation will have no effect.
|
||||
def unlock(key)
|
||||
Redis::Alfred.delete(key)
|
||||
true
|
||||
end
|
||||
|
||||
# Checks if the given key is currently locked.
|
||||
#
|
||||
# === Parameters
|
||||
# * +key+ - The key to check.
|
||||
#
|
||||
# === Returns
|
||||
# * +true+ if the key is locked.
|
||||
# * +false+ otherwise.
|
||||
def locked?(key)
|
||||
Redis::Alfred.exists?(key)
|
||||
end
|
||||
end
|
||||
55
research/chatwoot/lib/redis/redis_keys.rb
Normal file
55
research/chatwoot/lib/redis/redis_keys.rb
Normal file
@@ -0,0 +1,55 @@
|
||||
module Redis::RedisKeys
|
||||
## Inbox Keys
|
||||
# Array storing the ordered ids for agent round robin assignment
|
||||
ROUND_ROBIN_AGENTS = 'ROUND_ROBIN_AGENTS:%<inbox_id>d'.freeze
|
||||
|
||||
## Conversation keys
|
||||
# Detect whether to send an email reply to the conversation
|
||||
CONVERSATION_MAILER_KEY = 'CONVERSATION::%<conversation_id>d'.freeze
|
||||
# Whether a conversation is muted ?
|
||||
CONVERSATION_MUTE_KEY = 'CONVERSATION::%<id>d::MUTED'.freeze
|
||||
CONVERSATION_DRAFT_MESSAGE = 'CONVERSATION::%<id>d::DRAFT_MESSAGE'.freeze
|
||||
|
||||
## User Keys
|
||||
# SSO Auth Tokens
|
||||
USER_SSO_AUTH_TOKEN = 'USER_SSO_AUTH_TOKEN::%<user_id>d::%<token>s'.freeze
|
||||
|
||||
## Online Status Keys
|
||||
# hash containing user_id key and status as value
|
||||
ONLINE_STATUS = 'ONLINE_STATUS::%<account_id>d'.freeze
|
||||
# sorted set storing online presense of account contacts
|
||||
ONLINE_PRESENCE_CONTACTS = 'ONLINE_PRESENCE::%<account_id>d::CONTACTS'.freeze
|
||||
# sorted set storing online presense of account users
|
||||
ONLINE_PRESENCE_USERS = 'ONLINE_PRESENCE::%<account_id>d::USERS'.freeze
|
||||
|
||||
## Authorization Status Keys
|
||||
# Used to track token expiry and such issues for facebook slack integrations etc
|
||||
AUTHORIZATION_ERROR_COUNT = 'AUTHORIZATION_ERROR_COUNT:%<obj_type>s:%<obj_id>d'.freeze
|
||||
REAUTHORIZATION_REQUIRED = 'REAUTHORIZATION_REQUIRED:%<obj_type>s:%<obj_id>d'.freeze
|
||||
|
||||
## Internal Installation related keys
|
||||
CHATWOOT_INSTALLATION_ONBOARDING = 'CHATWOOT_INSTALLATION_ONBOARDING'.freeze
|
||||
CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING = 'CHATWOOT_CONFIG_RESET_WARNING'.freeze
|
||||
LATEST_CHATWOOT_VERSION = 'LATEST_CHATWOOT_VERSION'.freeze
|
||||
# Check if a message create with same source-id is in progress?
|
||||
MESSAGE_SOURCE_KEY = 'MESSAGE_SOURCE_KEY::%<id>s'.freeze
|
||||
OPENAI_CONVERSATION_KEY = 'OPEN_AI_CONVERSATION_KEY::V1::%<event_name>s::%<conversation_id>d::%<updated_at>d'.freeze
|
||||
|
||||
## Sempahores / Locks
|
||||
# We don't want to process messages from the same sender concurrently to prevent creating double conversations
|
||||
FACEBOOK_MESSAGE_MUTEX = 'FB_MESSAGE_CREATE_LOCK::%<sender_id>s::%<recipient_id>s'.freeze
|
||||
IG_MESSAGE_MUTEX = 'IG_MESSAGE_CREATE_LOCK::%<sender_id>s::%<ig_account_id>s'.freeze
|
||||
TIKTOK_MESSAGE_MUTEX = 'TIKTOK_MESSAGE_CREATE_LOCK::%<business_id>s::%<conversation_id>s'.freeze
|
||||
TIKTOK_REFRESH_TOKEN_MUTEX = 'TIKTOK_REFRESH_TOKEN_LOCK::%<channel_id>s'.freeze
|
||||
SLACK_MESSAGE_MUTEX = 'SLACK_MESSAGE_LOCK::%<conversation_id>s::%<reference_id>s'.freeze
|
||||
EMAIL_MESSAGE_MUTEX = 'EMAIL_CHANNEL_LOCK::%<inbox_id>s'.freeze
|
||||
CRM_PROCESS_MUTEX = 'CRM_PROCESS_MUTEX::%<hook_id>s'.freeze
|
||||
|
||||
## Auto Assignment Keys
|
||||
# Track conversation assignments to agents for rate limiting
|
||||
ASSIGNMENT_KEY = 'ASSIGNMENT::%<inbox_id>d::AGENT::%<agent_id>d::CONVERSATION::%<conversation_id>d'.freeze
|
||||
ASSIGNMENT_KEY_PATTERN = 'ASSIGNMENT::%<inbox_id>d::AGENT::%<agent_id>d::*'.freeze
|
||||
|
||||
## Account Email Rate Limiting
|
||||
ACCOUNT_OUTBOUND_EMAIL_COUNT_KEY = 'OUTBOUND_EMAIL_COUNT::%<account_id>d::%<date>s'.freeze
|
||||
end
|
||||
Reference in New Issue
Block a user