Fix single Redis connection being used across all threads (#18135)

* Fix single Redis connection being used across all Sidekiq threads

* Fix tests
This commit is contained in:
Eugen Rochko 2022-04-28 17:47:34 +02:00 committed by GitHub
parent 9bf04db23a
commit 3917353645
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 243 additions and 124 deletions

View file

@ -2,6 +2,8 @@
module Admin module Admin
class DashboardController < BaseController class DashboardController < BaseController
include Redisable
def index def index
@system_checks = Admin::SystemCheck.perform @system_checks = Admin::SystemCheck.perform
@time_period = (29.days.ago.to_date...Time.now.utc.to_date) @time_period = (29.days.ago.to_date...Time.now.utc.to_date)
@ -15,10 +17,10 @@ module Admin
def redis_info def redis_info
@redis_info ||= begin @redis_info ||= begin
if Redis.current.is_a?(Redis::Namespace) if redis.is_a?(Redis::Namespace)
Redis.current.redis.info redis.redis.info
else else
Redis.current.info redis.info
end end
end end
end end

View file

@ -3,6 +3,7 @@
class MediaProxyController < ApplicationController class MediaProxyController < ApplicationController
include RoutingHelper include RoutingHelper
include Authorization include Authorization
include Redisable
skip_before_action :store_current_location skip_before_action :store_current_location
skip_before_action :require_functional! skip_before_action :require_functional!
@ -45,7 +46,7 @@ class MediaProxyController < ApplicationController
end end
def lock_options def lock_options
{ redis: Redis.current, key: "media_download:#{params[:id]}", autorelease: 15.minutes.seconds } { redis: redis, key: "media_download:#{params[:id]}", autorelease: 15.minutes.seconds }
end end
def reject_media? def reject_media?

View file

@ -2,6 +2,7 @@
class Settings::ExportsController < Settings::BaseController class Settings::ExportsController < Settings::BaseController
include Authorization include Authorization
include Redisable
skip_before_action :require_functional! skip_before_action :require_functional!
@ -28,6 +29,6 @@ class Settings::ExportsController < Settings::BaseController
end end
def lock_options def lock_options
{ redis: Redis.current, key: "backup:#{current_user.id}" } { redis: redis, key: "backup:#{current_user.id}" }
end end
end end

View file

@ -4,6 +4,8 @@ module AccessTokenExtension
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
include Redisable
after_commit :push_to_streaming_api after_commit :push_to_streaming_api
end end
@ -16,6 +18,6 @@ module AccessTokenExtension
end end
def push_to_streaming_api def push_to_streaming_api
Redis.current.publish("timeline:access_token:#{id}", Oj.dump(event: :kill)) if revoked? || destroyed? redis.publish("timeline:access_token:#{id}", Oj.dump(event: :kill)) if revoked? || destroyed?
end end
end end

View file

@ -164,7 +164,7 @@ class ActivityPub::Activity
end end
def lock_or_fail(key, expire_after = 15.minutes.seconds) def lock_or_fail(key, expire_after = 15.minutes.seconds)
RedisLock.acquire({ redis: Redis.current, key: key, autorelease: expire_after }) do |lock| RedisLock.acquire({ redis: redis, key: key, autorelease: expire_after }) do |lock|
if lock.acquired? if lock.acquired?
yield yield
else else

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class DeliveryFailureTracker class DeliveryFailureTracker
include Redisable
FAILURE_DAYS_THRESHOLD = 7 FAILURE_DAYS_THRESHOLD = 7
def initialize(url_or_host) def initialize(url_or_host)
@ -8,21 +10,21 @@ class DeliveryFailureTracker
end end
def track_failure! def track_failure!
Redis.current.sadd(exhausted_deliveries_key, today) redis.sadd(exhausted_deliveries_key, today)
UnavailableDomain.create(domain: @host) if reached_failure_threshold? UnavailableDomain.create(domain: @host) if reached_failure_threshold?
end end
def track_success! def track_success!
Redis.current.del(exhausted_deliveries_key) redis.del(exhausted_deliveries_key)
UnavailableDomain.find_by(domain: @host)&.destroy UnavailableDomain.find_by(domain: @host)&.destroy
end end
def clear_failures! def clear_failures!
Redis.current.del(exhausted_deliveries_key) redis.del(exhausted_deliveries_key)
end end
def days def days
Redis.current.scard(exhausted_deliveries_key) || 0 redis.scard(exhausted_deliveries_key) || 0
end end
def available? def available?
@ -30,12 +32,14 @@ class DeliveryFailureTracker
end end
def exhausted_deliveries_days def exhausted_deliveries_days
@exhausted_deliveries_days ||= Redis.current.smembers(exhausted_deliveries_key).sort.map { |date| Date.new(date.slice(0, 4).to_i, date.slice(4, 2).to_i, date.slice(6, 2).to_i) } @exhausted_deliveries_days ||= redis.smembers(exhausted_deliveries_key).sort.map { |date| Date.new(date.slice(0, 4).to_i, date.slice(4, 2).to_i, date.slice(6, 2).to_i) }
end end
alias reset! track_success! alias reset! track_success!
class << self class << self
include Redisable
def without_unavailable(urls) def without_unavailable(urls)
unavailable_domains_map = Rails.cache.fetch('unavailable_domains') { UnavailableDomain.pluck(:domain).index_with(true) } unavailable_domains_map = Rails.cache.fetch('unavailable_domains') { UnavailableDomain.pluck(:domain).index_with(true) }
@ -54,7 +58,7 @@ class DeliveryFailureTracker
end end
def warning_domains def warning_domains
domains = Redis.current.keys(exhausted_deliveries_key_by('*')).map do |key| domains = redis.keys(exhausted_deliveries_key_by('*')).map do |key|
key.delete_prefix(exhausted_deliveries_key_by('')) key.delete_prefix(exhausted_deliveries_key_by(''))
end end
@ -62,7 +66,7 @@ class DeliveryFailureTracker
end end
def warning_domains_map def warning_domains_map
warning_domains.index_with { |domain| Redis.current.scard(exhausted_deliveries_key_by(domain)) } warning_domains.index_with { |domain| redis.scard(exhausted_deliveries_key_by(domain)) }
end end
private private

View file

@ -0,0 +1,47 @@
# frozen_string_literal: true
class RedisConfiguration
class << self
def with
pool.with { |redis| yield redis }
end
def pool
@pool ||= ConnectionPool.new(size: pool_size) { new.connection }
end
def pool_size
if Sidekiq.server?
Sidekiq.options[:concurrency]
else
ENV['MAX_THREADS'] || 5
end
end
end
def connection
if namespace?
Redis::Namespace.new(namespace, redis: raw_connection)
else
raw_connection
end
end
def namespace?
namespace.present?
end
def namespace
ENV.fetch('REDIS_NAMESPACE', nil)
end
def url
ENV['REDIS_URL']
end
private
def raw_connection
Redis.new(url: url, driver: :hiredis)
end
end

View file

@ -14,6 +14,8 @@
# #
class AccountConversation < ApplicationRecord class AccountConversation < ApplicationRecord
include Redisable
after_commit :push_to_streaming_api after_commit :push_to_streaming_api
belongs_to :account belongs_to :account
@ -109,7 +111,7 @@ class AccountConversation < ApplicationRecord
end end
def subscribed_to_timeline? def subscribed_to_timeline?
Redis.current.exists?("subscribed:#{streaming_channel}") redis.exists?("subscribed:#{streaming_channel}")
end end
def streaming_channel def streaming_channel

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class AccountSuggestions::GlobalSource < AccountSuggestions::Source class AccountSuggestions::GlobalSource < AccountSuggestions::Source
include Redisable
def key def key
:global :global
end end
@ -28,7 +30,7 @@ class AccountSuggestions::GlobalSource < AccountSuggestions::Source
end end
def account_ids_for_locale(locale) def account_ids_for_locale(locale)
Redis.current.zrevrange("follow_recommendations:#{locale}", 0, -1).map(&:to_i) redis.zrevrange("follow_recommendations:#{locale}", 0, -1).map(&:to_i)
end end
def to_ordered_list_key(account) def to_ordered_list_key(account)

View file

@ -6,6 +6,6 @@ module Redisable
private private
def redis def redis
Redis.current Thread.current[:redis] ||= RedisConfiguration.new.connection
end end
end end

View file

@ -24,6 +24,7 @@ class CustomFilter < ApplicationRecord
).freeze ).freeze
include Expireable include Expireable
include Redisable
belongs_to :account belongs_to :account
@ -51,7 +52,7 @@ class CustomFilter < ApplicationRecord
def remove_cache def remove_cache
Rails.cache.delete("filters:#{account_id}") Rails.cache.delete("filters:#{account_id}")
Redis.current.publish("timeline:#{account_id}", Oj.dump(event: :filters_changed)) redis.publish("timeline:#{account_id}", Oj.dump(event: :filters_changed))
end end
def context_must_be_valid def context_must_be_valid

View file

@ -19,6 +19,7 @@ class EncryptedMessage < ApplicationRecord
self.inheritance_column = nil self.inheritance_column = nil
include Paginable include Paginable
include Redisable
scope :up_to, ->(id) { where(arel_table[:id].lteq(id)) } scope :up_to, ->(id) { where(arel_table[:id].lteq(id)) }
@ -38,7 +39,7 @@ class EncryptedMessage < ApplicationRecord
end end
def subscribed_to_timeline? def subscribed_to_timeline?
Redis.current.exists?("subscribed:#{streaming_channel}") redis.exists?("subscribed:#{streaming_channel}")
end end
def streaming_channel def streaming_channel

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class FollowRecommendationFilter class FollowRecommendationFilter
include Redisable
KEYS = %i( KEYS = %i(
language language
status status
@ -17,7 +19,7 @@ class FollowRecommendationFilter
if params['status'] == 'suppressed' if params['status'] == 'suppressed'
Account.joins(:follow_recommendation_suppression).order(FollowRecommendationSuppression.arel_table[:id].desc).to_a Account.joins(:follow_recommendation_suppression).order(FollowRecommendationSuppression.arel_table[:id].desc).to_a
else else
account_ids = Redis.current.zrevrange("follow_recommendations:#{@language}", 0, -1).map(&:to_i) account_ids = redis.zrevrange("follow_recommendations:#{@language}", 0, -1).map(&:to_i)
accounts = Account.where(id: account_ids).index_by(&:id) accounts = Account.where(id: account_ids).index_by(&:id)
account_ids.map { |id| accounts[id] }.compact account_ids.map { |id| accounts[id] }.compact

View file

@ -52,6 +52,7 @@ class User < ApplicationRecord
include Settings::Extend include Settings::Extend
include UserRoles include UserRoles
include Redisable
# The home and list feeds will be stored in Redis for this amount # The home and list feeds will be stored in Redis for this amount
# of time, and status fan-out to followers will include only people # of time, and status fan-out to followers will include only people
@ -456,7 +457,7 @@ class User < ApplicationRecord
end end
def regenerate_feed! def regenerate_feed!
RegenerationWorker.perform_async(account_id) if Redis.current.set("account:#{account_id}:regeneration", true, nx: true, ex: 1.day.seconds) RegenerationWorker.perform_async(account_id) if redis.set("account:#{account_id}:regeneration", true, nx: true, ex: 1.day.seconds)
end end
def needs_feed_update? def needs_feed_update?

View file

@ -3,6 +3,7 @@
class ActivityPub::ProcessAccountService < BaseService class ActivityPub::ProcessAccountService < BaseService
include JsonLdHelper include JsonLdHelper
include DomainControlHelper include DomainControlHelper
include Redisable
# Should be called with confirmed valid JSON # Should be called with confirmed valid JSON
# and WebFinger-resolved username and domain # and WebFinger-resolved username and domain
@ -289,7 +290,7 @@ class ActivityPub::ProcessAccountService < BaseService
end end
def lock_options def lock_options
{ redis: Redis.current, key: "process_account:#{@uri}", autorelease: 15.minutes.seconds } { redis: redis, key: "process_account:#{@uri}", autorelease: 15.minutes.seconds }
end end
def process_tags def process_tags

View file

@ -2,6 +2,7 @@
class ActivityPub::ProcessStatusUpdateService < BaseService class ActivityPub::ProcessStatusUpdateService < BaseService
include JsonLdHelper include JsonLdHelper
include Redisable
def call(status, json) def call(status, json)
raise ArgumentError, 'Status has unsaved changes' if status.changed? raise ArgumentError, 'Status has unsaved changes' if status.changed?
@ -241,7 +242,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
end end
def lock_options def lock_options
{ redis: Redis.current, key: "create:#{@uri}", autorelease: 15.minutes.seconds } { redis: redis, key: "create:#{@uri}", autorelease: 15.minutes.seconds }
end end
def record_previous_edit! def record_previous_edit!

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class FanOutOnWriteService < BaseService class FanOutOnWriteService < BaseService
include Redisable
# Push a status into home and mentions feeds # Push a status into home and mentions feeds
# @param [Status] status # @param [Status] status
# @param [Hash] options # @param [Hash] options
@ -99,20 +101,20 @@ class FanOutOnWriteService < BaseService
def broadcast_to_hashtag_streams! def broadcast_to_hashtag_streams!
@status.tags.pluck(:name).each do |hashtag| @status.tags.pluck(:name).each do |hashtag|
Redis.current.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", anonymous_payload) redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", anonymous_payload)
Redis.current.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", anonymous_payload) if @status.local? redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", anonymous_payload) if @status.local?
end end
end end
def broadcast_to_public_streams! def broadcast_to_public_streams!
return if @status.reply? && @status.in_reply_to_account_id != @account.id return if @status.reply? && @status.in_reply_to_account_id != @account.id
Redis.current.publish('timeline:public', anonymous_payload) redis.publish('timeline:public', anonymous_payload)
Redis.current.publish(@status.local? ? 'timeline:public:local' : 'timeline:public:remote', anonymous_payload) redis.publish(@status.local? ? 'timeline:public:local' : 'timeline:public:remote', anonymous_payload)
if @status.with_media? if @status.with_media?
Redis.current.publish('timeline:public:media', anonymous_payload) redis.publish('timeline:public:media', anonymous_payload)
Redis.current.publish(@status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', anonymous_payload) redis.publish(@status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', anonymous_payload)
end end
end end

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class FetchLinkCardService < BaseService class FetchLinkCardService < BaseService
include Redisable
URL_PATTERN = %r{ URL_PATTERN = %r{
(#{Twitter::TwitterText::Regex[:valid_url_preceding_chars]}) # $1 preceding chars (#{Twitter::TwitterText::Regex[:valid_url_preceding_chars]}) # $1 preceding chars
( # $2 URL ( # $2 URL
@ -155,6 +157,6 @@ class FetchLinkCardService < BaseService
end end
def lock_options def lock_options
{ redis: Redis.current, key: "fetch:#{@original_url}", autorelease: 15.minutes.seconds } { redis: redis, key: "fetch:#{@original_url}", autorelease: 15.minutes.seconds }
end end
end end

View file

@ -1,9 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class PrecomputeFeedService < BaseService class PrecomputeFeedService < BaseService
include Redisable
def call(account) def call(account)
FeedManager.instance.populate_home(account) FeedManager.instance.populate_home(account)
ensure ensure
Redis.current.del("account:#{account.id}:regeneration") redis.del("account:#{account.id}:regeneration")
end end
end end

View file

@ -146,6 +146,6 @@ class RemoveStatusService < BaseService
end end
def lock_options def lock_options
{ redis: Redis.current, key: "distribute:#{@status.id}", autorelease: 5.minutes.seconds } { redis: redis, key: "distribute:#{@status.id}", autorelease: 5.minutes.seconds }
end end
end end

View file

@ -4,6 +4,7 @@ class ResolveAccountService < BaseService
include JsonLdHelper include JsonLdHelper
include DomainControlHelper include DomainControlHelper
include WebfingerHelper include WebfingerHelper
include Redisable
# Find or create an account record for a remote user. When creating, # Find or create an account record for a remote user. When creating,
# look up the user's webfinger and fetch ActivityPub data # look up the user's webfinger and fetch ActivityPub data
@ -147,6 +148,6 @@ class ResolveAccountService < BaseService
end end
def lock_options def lock_options
{ redis: Redis.current, key: "resolve:#{@username}@#{@domain}", autorelease: 15.minutes.seconds } { redis: redis, key: "resolve:#{@username}@#{@domain}", autorelease: 15.minutes.seconds }
end end
end end

View file

@ -3,6 +3,7 @@
class VoteService < BaseService class VoteService < BaseService
include Authorization include Authorization
include Payloadable include Payloadable
include Redisable
def call(account, poll, choices) def call(account, poll, choices)
authorize_with account, poll, :vote? authorize_with account, poll, :vote?
@ -77,6 +78,6 @@ class VoteService < BaseService
end end
def lock_options def lock_options
{ redis: Redis.current, key: "vote:#{@poll.id}:#{@account.id}" } { redis: redis, key: "vote:#{@poll.id}:#{@account.id}" }
end end
end end

View file

@ -2,9 +2,10 @@
class DistributionWorker class DistributionWorker
include Sidekiq::Worker include Sidekiq::Worker
include Redisable
def perform(status_id, options = {}) def perform(status_id, options = {})
RedisLock.acquire(redis: Redis.current, key: "distribute:#{status_id}", autorelease: 5.minutes.seconds) do |lock| RedisLock.acquire(redis: redis, key: "distribute:#{status_id}", autorelease: 5.minutes.seconds) do |lock|
if lock.acquired? if lock.acquired?
FanOutOnWriteService.new.call(Status.find(status_id), **options.symbolize_keys) FanOutOnWriteService.new.call(Status.find(status_id), **options.symbolize_keys)
else else

View file

@ -2,12 +2,13 @@
class MergeWorker class MergeWorker
include Sidekiq::Worker include Sidekiq::Worker
include Redisable
def perform(from_account_id, into_account_id) def perform(from_account_id, into_account_id)
FeedManager.instance.merge_into_home(Account.find(from_account_id), Account.find(into_account_id)) FeedManager.instance.merge_into_home(Account.find(from_account_id), Account.find(into_account_id))
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
true true
ensure ensure
Redis.current.del("account:#{into_account_id}:regeneration") redis.del("account:#{into_account_id}:regeneration")
end end
end end

View file

@ -2,6 +2,7 @@
class Scheduler::AccountsStatusesCleanupScheduler class Scheduler::AccountsStatusesCleanupScheduler
include Sidekiq::Worker include Sidekiq::Worker
include Redisable
# This limit is mostly to be nice to the fediverse at large and not # This limit is mostly to be nice to the fediverse at large and not
# generate too much traffic. # generate too much traffic.
@ -83,14 +84,14 @@ class Scheduler::AccountsStatusesCleanupScheduler
end end
def last_processed_id def last_processed_id
Redis.current.get('account_statuses_cleanup_scheduler:last_account_id') redis.get('account_statuses_cleanup_scheduler:last_account_id')
end end
def save_last_processed_id(id) def save_last_processed_id(id)
if id.nil? if id.nil?
Redis.current.del('account_statuses_cleanup_scheduler:last_account_id') redis.del('account_statuses_cleanup_scheduler:last_account_id')
else else
Redis.current.set('account_statuses_cleanup_scheduler:last_account_id', id, ex: 1.hour.seconds) redis.set('account_statuses_cleanup_scheduler:last_account_id', id, ex: 1.hour.seconds)
end end
end end
end end

View file

@ -35,6 +35,7 @@ require_relative '../lib/paperclip/response_with_limit_adapter'
require_relative '../lib/terrapin/multi_pipe_extensions' require_relative '../lib/terrapin/multi_pipe_extensions'
require_relative '../lib/mastodon/snowflake' require_relative '../lib/mastodon/snowflake'
require_relative '../lib/mastodon/version' require_relative '../lib/mastodon/version'
require_relative '../lib/mastodon/rack_middleware'
require_relative '../lib/devise/two_factor_ldap_authenticatable' require_relative '../lib/devise/two_factor_ldap_authenticatable'
require_relative '../lib/devise/two_factor_pam_authenticatable' require_relative '../lib/devise/two_factor_pam_authenticatable'
require_relative '../lib/chewy/strategy/custom_sidekiq' require_relative '../lib/chewy/strategy/custom_sidekiq'
@ -164,6 +165,7 @@ module Mastodon
config.middleware.use Rack::Attack config.middleware.use Rack::Attack
config.middleware.use Rack::Deflater config.middleware.use Rack::Deflater
config.middleware.use Mastodon::RackMiddleware
config.to_prepare do config.to_prepare do
Doorkeeper::AuthorizationsController.layout 'modal' Doorkeeper::AuthorizationsController.layout 'modal'

View file

@ -1,14 +0,0 @@
# frozen_string_literal: true
redis_connection = Redis.new(
url: ENV['REDIS_URL'],
driver: :hiredis
)
namespace = ENV.fetch('REDIS_NAMESPACE') { nil }
if namespace
Redis.current = Redis::Namespace.new(namespace, redis: redis_connection)
else
Redis.current = redis_connection
end

View file

@ -1,12 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative '../../lib/sidekiq_error_handler' require_relative '../../lib/mastodon/sidekiq_middleware'
Sidekiq.configure_server do |config| Sidekiq.configure_server do |config|
config.redis = REDIS_SIDEKIQ_PARAMS config.redis = REDIS_SIDEKIQ_PARAMS
config.server_middleware do |chain| config.server_middleware do |chain|
chain.add SidekiqErrorHandler chain.add Mastodon::SidekiqMiddleware
end end
config.server_middleware do |chain| config.server_middleware do |chain|

View file

@ -7,6 +7,7 @@ require_relative 'cli_helper'
module Mastodon module Mastodon
class FeedsCLI < Thor class FeedsCLI < Thor
include CLIHelper include CLIHelper
include Redisable
def self.exit_on_failure? def self.exit_on_failure?
true true
@ -51,10 +52,10 @@ module Mastodon
desc 'clear', 'Remove all home and list feeds from Redis' desc 'clear', 'Remove all home and list feeds from Redis'
def clear def clear
keys = Redis.current.keys('feed:*') keys = redis.keys('feed:*')
Redis.current.pipelined do redis.pipelined do
keys.each { |key| Redis.current.del(key) } keys.each { |key| redis.del(key) }
end end
say('OK', :green) say('OK', :green)

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
class Mastodon::RackMiddleware
def initialize(app)
@app = app
end
def call(env)
@app.call(env)
ensure
clean_up_sockets!
end
private
def clean_up_sockets!
clean_up_redis_socket!
clean_up_statsd_socket!
end
def clean_up_redis_socket!
Thread.current[:redis]&.close
Thread.current[:redis] = nil
end
def clean_up_statsd_socket!
Thread.current[:statsd_socket]&.close
Thread.current[:statsd_socket] = nil
end
end

View file

@ -11,7 +11,8 @@ def setup_redis_env_url(prefix = nil, defaults = true)
port = ENV.fetch(prefix + 'REDIS_PORT') { 6379 if defaults } port = ENV.fetch(prefix + 'REDIS_PORT') { 6379 if defaults }
db = ENV.fetch(prefix + 'REDIS_DB') { 0 if defaults } db = ENV.fetch(prefix + 'REDIS_DB') { 0 if defaults }
ENV[prefix + 'REDIS_URL'] = if [password, host, port, db].all?(&:nil?) ENV[prefix + 'REDIS_URL'] = begin
if [password, host, port, db].all?(&:nil?)
ENV['REDIS_URL'] ENV['REDIS_URL']
else else
Addressable::URI.parse("redis://#{host}:#{port}/#{db}").tap do |uri| Addressable::URI.parse("redis://#{host}:#{port}/#{db}").tap do |uri|
@ -19,6 +20,7 @@ def setup_redis_env_url(prefix = nil, defaults = true)
end.normalize.to_str end.normalize.to_str
end end
end end
end
setup_redis_env_url setup_redis_env_url
setup_redis_env_url(:cache, false) setup_redis_env_url(:cache, false)
@ -33,6 +35,8 @@ REDIS_CACHE_PARAMS = {
url: ENV['CACHE_REDIS_URL'], url: ENV['CACHE_REDIS_URL'],
expires_in: 10.minutes, expires_in: 10.minutes,
namespace: cache_namespace, namespace: cache_namespace,
pool_size: Sidekiq.server? ? Sidekiq.options[:concurrency] : Integer(ENV['MAX_THREADS'] || 5),
pool_timeout: 5,
}.freeze }.freeze
REDIS_SIDEKIQ_PARAMS = { REDIS_SIDEKIQ_PARAMS = {
@ -40,3 +44,7 @@ REDIS_SIDEKIQ_PARAMS = {
url: ENV['SIDEKIQ_REDIS_URL'], url: ENV['SIDEKIQ_REDIS_URL'],
namespace: sidekiq_namespace, namespace: sidekiq_namespace,
}.freeze }.freeze
if Rails.env.test?
ENV['REDIS_NAMESPACE'] = "mastodon_test#{ENV['TEST_ENV_NUMBER']}"
end

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class SidekiqErrorHandler class Mastodon::SidekiqMiddleware
BACKTRACE_LIMIT = 3 BACKTRACE_LIMIT = 3
def call(*) def call(*)
@ -10,9 +10,7 @@ class SidekiqErrorHandler
rescue => e rescue => e
limit_backtrace_and_raise(e) limit_backtrace_and_raise(e)
ensure ensure
socket = Thread.current[:statsd_socket] clean_up_sockets!
socket&.close
Thread.current[:statsd_socket] = nil
end end
private private
@ -21,4 +19,19 @@ class SidekiqErrorHandler
exception.set_backtrace(exception.backtrace.first(BACKTRACE_LIMIT)) exception.set_backtrace(exception.backtrace.first(BACKTRACE_LIMIT))
raise exception raise exception
end end
def clean_up_sockets!
clean_up_redis_socket!
clean_up_statsd_socket!
end
def clean_up_redis_socket!
Thread.current[:redis]&.close
Thread.current[:redis] = nil
end
def clean_up_statsd_socket!
Thread.current[:statsd_socket]&.close
Thread.current[:statsd_socket] = nil
end
end end

View file

@ -65,22 +65,22 @@ describe ApplicationController, type: :controller do
get :show get :show
expect_updated_sign_in_at(user) expect_updated_sign_in_at(user)
expect(Redis.current.get("account:#{user.account_id}:regeneration")).to eq 'true' expect(redis.get("account:#{user.account_id}:regeneration")).to eq 'true'
expect(RegenerationWorker).to have_received(:perform_async) expect(RegenerationWorker).to have_received(:perform_async)
end end
it 'sets the regeneration marker to expire' do it 'sets the regeneration marker to expire' do
allow(RegenerationWorker).to receive(:perform_async) allow(RegenerationWorker).to receive(:perform_async)
get :show get :show
expect(Redis.current.ttl("account:#{user.account_id}:regeneration")).to be >= 0 expect(redis.ttl("account:#{user.account_id}:regeneration")).to be >= 0
end end
it 'regenerates feed when sign in is older than two weeks' do it 'regenerates feed when sign in is older than two weeks' do
get :show get :show
expect_updated_sign_in_at(user) expect_updated_sign_in_at(user)
expect(Redis.current.zcard(FeedManager.instance.key(:home, user.account_id))).to eq 3 expect(redis.zcard(FeedManager.instance.key(:home, user.account_id))).to eq 3
expect(Redis.current.get("account:#{user.account_id}:regeneration")).to be_nil expect(redis.get("account:#{user.account_id}:regeneration")).to be_nil
end end
end end

View file

@ -84,9 +84,9 @@ RSpec.describe ActivityPub::Activity::Move do
context 'when a Move has been recently processed' do context 'when a Move has been recently processed' do
around do |example| around do |example|
Redis.current.set("move_in_progress:#{old_account.id}", true, nx: true, ex: 7.days.seconds) redis.set("move_in_progress:#{old_account.id}", true, nx: true, ex: 7.days.seconds)
example.run example.run
Redis.current.del("move_in_progress:#{old_account.id}") redis.del("move_in_progress:#{old_account.id}")
end end
it 'does not set moved account on old account' do it 'does not set moved account on old account' do

View file

@ -22,7 +22,7 @@ describe DeliveryFailureTracker do
describe '#track_failure!' do describe '#track_failure!' do
it 'marks URL as unavailable after 7 days of being called' do it 'marks URL as unavailable after 7 days of being called' do
6.times { |i| Redis.current.sadd('exhausted_deliveries:example.com', i) } 6.times { |i| redis.sadd('exhausted_deliveries:example.com', i) }
subject.track_failure! subject.track_failure!
expect(subject.days).to eq 7 expect(subject.days).to eq 7

View file

@ -195,11 +195,11 @@ RSpec.describe FeedManager do
account = Fabricate(:account) account = Fabricate(:account)
status = Fabricate(:status) status = Fabricate(:status)
members = FeedManager::MAX_ITEMS.times.map { |count| [count, count] } members = FeedManager::MAX_ITEMS.times.map { |count| [count, count] }
Redis.current.zadd("feed:home:#{account.id}", members) redis.zadd("feed:home:#{account.id}", members)
FeedManager.instance.push_to_home(account, status) FeedManager.instance.push_to_home(account, status)
expect(Redis.current.zcard("feed:home:#{account.id}")).to eq FeedManager::MAX_ITEMS expect(redis.zcard("feed:home:#{account.id}")).to eq FeedManager::MAX_ITEMS
end end
context 'reblogs' do context 'reblogs' do
@ -424,7 +424,7 @@ RSpec.describe FeedManager do
FeedManager.instance.merge_into_home(account, reblog.account) FeedManager.instance.merge_into_home(account, reblog.account)
expect(Redis.current.zscore("feed:home:0", reblog.id)).to eq nil expect(redis.zscore("feed:home:0", reblog.id)).to eq nil
end end
end end
@ -440,13 +440,13 @@ RSpec.describe FeedManager do
FeedManager.instance.push_to_home(receiver, status) FeedManager.instance.push_to_home(receiver, status)
# The reblogging status should show up under normal conditions. # The reblogging status should show up under normal conditions.
expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s) expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
FeedManager.instance.unpush_from_home(receiver, status) FeedManager.instance.unpush_from_home(receiver, status)
# Restore original status # Restore original status
expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s) expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s)
expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to include(reblogged.id.to_s) expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(reblogged.id.to_s)
end end
it 'removes a reblogged status if it was only reblogged once' do it 'removes a reblogged status if it was only reblogged once' do
@ -456,11 +456,11 @@ RSpec.describe FeedManager do
FeedManager.instance.push_to_home(receiver, status) FeedManager.instance.push_to_home(receiver, status)
# The reblogging status should show up under normal conditions. # The reblogging status should show up under normal conditions.
expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [status.id.to_s] expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [status.id.to_s]
FeedManager.instance.unpush_from_home(receiver, status) FeedManager.instance.unpush_from_home(receiver, status)
expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to be_empty expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to be_empty
end end
it 'leaves a multiply-reblogged status if another reblog was in feed' do it 'leaves a multiply-reblogged status if another reblog was in feed' do
@ -472,13 +472,13 @@ RSpec.describe FeedManager do
end end
# The reblogging status should show up under normal conditions. # The reblogging status should show up under normal conditions.
expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.first.id.to_s] expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.first.id.to_s]
reblogs[0...-1].each do |reblog| reblogs[0...-1].each do |reblog|
FeedManager.instance.unpush_from_home(receiver, reblog) FeedManager.instance.unpush_from_home(receiver, reblog)
end end
expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.last.id.to_s] expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.last.id.to_s]
end end
it 'sends push updates' do it 'sends push updates' do
@ -486,11 +486,11 @@ RSpec.describe FeedManager do
FeedManager.instance.push_to_home(receiver, status) FeedManager.instance.push_to_home(receiver, status)
allow(Redis.current).to receive_messages(publish: nil) allow(redis).to receive_messages(publish: nil)
FeedManager.instance.unpush_from_home(receiver, status) FeedManager.instance.unpush_from_home(receiver, status)
deletion = Oj.dump(event: :delete, payload: status.id.to_s) deletion = Oj.dump(event: :delete, payload: status.id.to_s)
expect(Redis.current).to have_received(:publish).with("timeline:#{receiver.id}", deletion) expect(redis).to have_received(:publish).with("timeline:#{receiver.id}", deletion)
end end
end end
@ -508,14 +508,14 @@ RSpec.describe FeedManager do
before do before do
[status_1, status_3, status_5, status_6, status_7].each do |status| [status_1, status_3, status_5, status_6, status_7].each do |status|
Redis.current.zadd("feed:home:#{account.id}", status.id, status.id) redis.zadd("feed:home:#{account.id}", status.id, status.id)
end end
end end
it 'correctly cleans the home timeline' do it 'correctly cleans the home timeline' do
FeedManager.instance.clear_from_home(account, target_account) FeedManager.instance.clear_from_home(account, target_account)
expect(Redis.current.zrange("feed:home:#{account.id}", 0, -1)).to eq [status_1.id.to_s, status_7.id.to_s] expect(redis.zrange("feed:home:#{account.id}", 0, -1)).to eq [status_1.id.to_s, status_7.id.to_s]
end end
end end
end end

View file

@ -15,7 +15,7 @@ RSpec.describe HomeFeed, type: :model do
context 'when feed is generated' do context 'when feed is generated' do
before do before do
Redis.current.zadd( redis.zadd(
FeedManager.instance.key(:home, account.id), FeedManager.instance.key(:home, account.id),
[[4, 4], [3, 3], [2, 2], [1, 1]] [[4, 4], [3, 3], [2, 2], [1, 1]]
) )
@ -31,7 +31,7 @@ RSpec.describe HomeFeed, type: :model do
context 'when feed is being generated' do context 'when feed is being generated' do
before do before do
Redis.current.set("account:#{account.id}:regeneration", true) redis.set("account:#{account.id}:regeneration", true)
end end
it 'returns nothing' do it 'returns nothing' do

View file

@ -13,7 +13,6 @@ Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
ActiveRecord::Migration.maintain_test_schema! ActiveRecord::Migration.maintain_test_schema!
WebMock.disable_net_connect!(allow: Chewy.settings[:host]) WebMock.disable_net_connect!(allow: Chewy.settings[:host])
Redis.current = Redis::Namespace.new("mastodon_test#{ENV['TEST_ENV_NUMBER']}", redis: Redis.current)
Sidekiq::Testing.inline! Sidekiq::Testing.inline!
Sidekiq.logger = nil Sidekiq.logger = nil
@ -44,6 +43,7 @@ RSpec.configure do |config|
config.include Devise::Test::ControllerHelpers, type: :view config.include Devise::Test::ControllerHelpers, type: :view
config.include Paperclip::Shoulda::Matchers config.include Paperclip::Shoulda::Matchers
config.include ActiveSupport::Testing::TimeHelpers config.include ActiveSupport::Testing::TimeHelpers
config.include Redisable
config.before :each, type: :feature do config.before :each, type: :feature do
https = ENV['LOCAL_HTTPS'] == 'true' https = ENV['LOCAL_HTTPS'] == 'true'
@ -60,7 +60,7 @@ RSpec.configure do |config|
config.after :each do config.after :each do
Rails.cache.clear Rails.cache.clear
Redis.current.del(Redis.current.keys) redis.del(redis.keys)
end end
end end

View file

@ -14,7 +14,7 @@ RSpec.describe AfterBlockService, type: :service do
let(:home_timeline_key) { FeedManager.instance.key(:home, account.id) } let(:home_timeline_key) { FeedManager.instance.key(:home, account.id) }
before do before do
Redis.current.del(home_timeline_key) redis.del(home_timeline_key)
end end
it "clears account's statuses" do it "clears account's statuses" do
@ -23,7 +23,7 @@ RSpec.describe AfterBlockService, type: :service do
FeedManager.instance.push_to_home(account, other_account_reblog) FeedManager.instance.push_to_home(account, other_account_reblog)
expect { subject }.to change { expect { subject }.to change {
Redis.current.zrange(home_timeline_key, 0, -1) redis.zrange(home_timeline_key, 0, -1)
}.from([status.id.to_s, other_account_status.id.to_s, other_account_reblog.id.to_s]).to([other_account_status.id.to_s]) }.from([status.id.to_s, other_account_status.id.to_s, other_account_reblog.id.to_s]).to([other_account_status.id.to_s])
end end
end end
@ -33,7 +33,7 @@ RSpec.describe AfterBlockService, type: :service do
let(:list_timeline_key) { FeedManager.instance.key(:list, list.id) } let(:list_timeline_key) { FeedManager.instance.key(:list, list.id) }
before do before do
Redis.current.del(list_timeline_key) redis.del(list_timeline_key)
end end
it "clears account's statuses" do it "clears account's statuses" do
@ -42,7 +42,7 @@ RSpec.describe AfterBlockService, type: :service do
FeedManager.instance.push_to_list(list, other_account_reblog) FeedManager.instance.push_to_list(list, other_account_reblog)
expect { subject }.to change { expect { subject }.to change {
Redis.current.zrange(list_timeline_key, 0, -1) redis.zrange(list_timeline_key, 0, -1)
}.from([status.id.to_s, other_account_status.id.to_s, other_account_reblog.id.to_s]).to([other_account_status.id.to_s]) }.from([status.id.to_s, other_account_status.id.to_s, other_account_reblog.id.to_s]).to([other_account_status.id.to_s])
end end
end end

View file

@ -12,7 +12,7 @@ RSpec.describe BatchedRemoveStatusService, type: :service do
let(:status2) { PostStatusService.new.call(alice, text: 'Another status') } let(:status2) { PostStatusService.new.call(alice, text: 'Another status') }
before do before do
allow(Redis.current).to receive_messages(publish: nil) allow(redis).to receive_messages(publish: nil)
stub_request(:post, 'http://example.com/inbox').to_return(status: 200) stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
@ -40,11 +40,11 @@ RSpec.describe BatchedRemoveStatusService, type: :service do
end end
it 'notifies streaming API of followers' do it 'notifies streaming API of followers' do
expect(Redis.current).to have_received(:publish).with("timeline:#{jeff.id}", any_args).at_least(:once) expect(redis).to have_received(:publish).with("timeline:#{jeff.id}", any_args).at_least(:once)
end end
it 'notifies streaming API of public timeline' do it 'notifies streaming API of public timeline' do
expect(Redis.current).to have_received(:publish).with('timeline:public', any_args).at_least(:once) expect(redis).to have_received(:publish).with('timeline:public', any_args).at_least(:once)
end end
it 'sends delete activity to followers' do it 'sends delete activity to followers' do

View file

@ -18,7 +18,7 @@ RSpec.describe FanOutOnWriteService, type: :service do
ProcessMentionsService.new.call(status) ProcessMentionsService.new.call(status)
ProcessHashtagsService.new.call(status) ProcessHashtagsService.new.call(status)
allow(Redis.current).to receive(:publish) allow(redis).to receive(:publish)
subject.call(status) subject.call(status)
end end
@ -40,13 +40,13 @@ RSpec.describe FanOutOnWriteService, type: :service do
end end
it 'is broadcast to the hashtag stream' do it 'is broadcast to the hashtag stream' do
expect(Redis.current).to have_received(:publish).with('timeline:hashtag:hoge', anything) expect(redis).to have_received(:publish).with('timeline:hashtag:hoge', anything)
expect(Redis.current).to have_received(:publish).with('timeline:hashtag:hoge:local', anything) expect(redis).to have_received(:publish).with('timeline:hashtag:hoge:local', anything)
end end
it 'is broadcast to the public stream' do it 'is broadcast to the public stream' do
expect(Redis.current).to have_received(:publish).with('timeline:public', anything) expect(redis).to have_received(:publish).with('timeline:public', anything)
expect(Redis.current).to have_received(:publish).with('timeline:public:local', anything) expect(redis).to have_received(:publish).with('timeline:public:local', anything)
end end
end end
@ -66,8 +66,8 @@ RSpec.describe FanOutOnWriteService, type: :service do
end end
it 'is not broadcast publicly' do it 'is not broadcast publicly' do
expect(Redis.current).to_not have_received(:publish).with('timeline:hashtag:hoge', anything) expect(redis).to_not have_received(:publish).with('timeline:hashtag:hoge', anything)
expect(Redis.current).to_not have_received(:publish).with('timeline:public', anything) expect(redis).to_not have_received(:publish).with('timeline:public', anything)
end end
end end
@ -84,8 +84,8 @@ RSpec.describe FanOutOnWriteService, type: :service do
end end
it 'is not broadcast publicly' do it 'is not broadcast publicly' do
expect(Redis.current).to_not have_received(:publish).with('timeline:hashtag:hoge', anything) expect(redis).to_not have_received(:publish).with('timeline:hashtag:hoge', anything)
expect(Redis.current).to_not have_received(:publish).with('timeline:public', anything) expect(redis).to_not have_received(:publish).with('timeline:public', anything)
end end
end end
@ -105,8 +105,8 @@ RSpec.describe FanOutOnWriteService, type: :service do
end end
it 'is not broadcast publicly' do it 'is not broadcast publicly' do
expect(Redis.current).to_not have_received(:publish).with('timeline:hashtag:hoge', anything) expect(redis).to_not have_received(:publish).with('timeline:hashtag:hoge', anything)
expect(Redis.current).to_not have_received(:publish).with('timeline:public', anything) expect(redis).to_not have_received(:publish).with('timeline:public', anything)
end end
end end
end end

View file

@ -12,7 +12,7 @@ RSpec.describe MuteService, type: :service do
let(:home_timeline_key) { FeedManager.instance.key(:home, account.id) } let(:home_timeline_key) { FeedManager.instance.key(:home, account.id) }
before do before do
Redis.current.del(home_timeline_key) redis.del(home_timeline_key)
end end
it "clears account's statuses" do it "clears account's statuses" do
@ -20,7 +20,7 @@ RSpec.describe MuteService, type: :service do
FeedManager.instance.push_to_home(account, other_account_status) FeedManager.instance.push_to_home(account, other_account_status)
expect { subject }.to change { expect { subject }.to change {
Redis.current.zrange(home_timeline_key, 0, -1) redis.zrange(home_timeline_key, 0, -1)
}.from([status.id.to_s, other_account_status.id.to_s]).to([other_account_status.id.to_s]) }.from([status.id.to_s, other_account_status.id.to_s]).to([other_account_status.id.to_s])
end end
end end

View file

@ -13,7 +13,7 @@ RSpec.describe PrecomputeFeedService, type: :service do
subject.call(account) subject.call(account)
expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), status.id)).to be_within(0.1).of(status.id.to_f) expect(redis.zscore(FeedManager.instance.key(:home, account.id), status.id)).to be_within(0.1).of(status.id.to_f)
end end
it 'does not raise an error even if it could not find any status' do it 'does not raise an error even if it could not find any status' do
@ -30,7 +30,7 @@ RSpec.describe PrecomputeFeedService, type: :service do
subject.call(account) subject.call(account)
expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to eq nil expect(redis.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to eq nil
end end
end end
end end

View file

@ -7,17 +7,17 @@ describe Scheduler::FeedCleanupScheduler do
let!(:inactive_user) { Fabricate(:user, current_sign_in_at: 22.days.ago) } let!(:inactive_user) { Fabricate(:user, current_sign_in_at: 22.days.ago) }
it 'clears feeds of inactives' do it 'clears feeds of inactives' do
Redis.current.zadd(feed_key_for(inactive_user), 1, 1) redis.zadd(feed_key_for(inactive_user), 1, 1)
Redis.current.zadd(feed_key_for(active_user), 1, 1) redis.zadd(feed_key_for(active_user), 1, 1)
Redis.current.zadd(feed_key_for(inactive_user, 'reblogs'), 2, 2) redis.zadd(feed_key_for(inactive_user, 'reblogs'), 2, 2)
Redis.current.sadd(feed_key_for(inactive_user, 'reblogs:2'), 3) redis.sadd(feed_key_for(inactive_user, 'reblogs:2'), 3)
subject.perform subject.perform
expect(Redis.current.zcard(feed_key_for(inactive_user))).to eq 0 expect(redis.zcard(feed_key_for(inactive_user))).to eq 0
expect(Redis.current.zcard(feed_key_for(active_user))).to eq 1 expect(redis.zcard(feed_key_for(active_user))).to eq 1
expect(Redis.current.exists?(feed_key_for(inactive_user, 'reblogs'))).to be false expect(redis.exists?(feed_key_for(inactive_user, 'reblogs'))).to be false
expect(Redis.current.exists?(feed_key_for(inactive_user, 'reblogs:2'))).to be false expect(redis.exists?(feed_key_for(inactive_user, 'reblogs:2'))).to be false
end end
def feed_key_for(user, subtype = nil) def feed_key_for(user, subtype = nil)