Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sentinel support #79

Merged
merged 8 commits into from Oct 25, 2016
Merged

Sentinel support #79

merged 8 commits into from Oct 25, 2016

Conversation

brodock
Copy link
Contributor

@brodock brodock commented Oct 20, 2016

Sentinel with the ruby client is a little tricky to get right, because they use a special convention in "Redis URL" to pass additional params.

When you have sentinel, it always handle the first connection your client will make to, and it redirects your to a working "master".

For a production setup, you need at least 3 independent machines, each one running redis and sentinel. One of these machines will have a master redis, the others will have slave redis.

For sake of testing, you can do 1 machine with 1 sentinel and 1 master and 1 slave redis running in different ports.

Here is a minimal sentinel.conf file:

bind 0.0.0.0
port 26379
sentinel monitor myredis 127.0.0.1 6379 1
sentinel down-after-milliseconds myredis 10000
sentinel auth-pass myredis "mypassword"

myredis is the <master-name> as stated in sentinel.conf documentation: http://download.redis.io/redis-stable/sentinel.conf

With that setup, here is how you connect to sentinel with Redis client (in ruby):

client = ::Redis.new(url: 'redis://:mypassword@myredis/', sentinels: [host: '127.0.0.1', port: 26379]
client.info # to make sure it worked, check output.

What can go wrong?
This is the most common error: Redis::CannotConnectError: No sentinels available. and the most frustating one to debug.

It can mean literally anything. Your password is wrong, you are pointing to an unreachable address, invalid port, you typed the wrong "master-name" in the url, etc.


With this PR you can make MailRoom use Sentinel to connect to Redis for both Sidekiq and Arbitration

Here is a sample config.yml:

---
:mailboxes:
  -
    :email: "user1@domain.tld"
    :password: "mypassword"
    :name: "inbox"
    :search_command: 'NEW'
    :host: 127.0.0.1
    :port: 1430
    :ssl: false
    :delivery_method: sidekiq
    :delivery_options:
      :redis_url: "redis://:mypassword@myredis:6379"
      :sentinels:
        -
          :host: 127.0.0.1
          :port: 26379
    :arbitration_method: redis
    :arbitration_options:
      :redis_url: "redis://:mypassword@myredis:6379"
      :sentinels:
        -
          :host: 127.0.0.1
          :port: 26379

fixes #55

@brodock brodock mentioned this pull request Oct 20, 2016
@maxlazio
Copy link

@DouweM Could you check this out?

@@ -0,0 +1,31 @@
# Cache gems in between builds
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MailRoom uses travis ci: https://travis-ci.org/tpitale/mail_room.

Are there reasons why we would also run gitlab ci?

Copy link
Contributor Author

@brodock brodock Oct 20, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @tpitale, this was there because my initial fork was running here: https://gitlab.com/brodock/mail_room/tree/sentinel-support so I needed that file to run it with our CI.

It should do no harm, but I can remove it from the commit if you want. (as a side-effect, anyone who clone / fork / mirror using GitLab gets CI enabled automatically if the file stays)

def initialize(mailbox)
redis_url = mailbox.arbitration_options[:redis_url] || "redis://localhost:6379"
namespace = mailbox.arbitration_options[:namespace]
sentinels = mailbox.arbitration_options[:sentinels]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the structure of sentinels? Would you mind updating the README to include usage for this configuration?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

redis = ::Redis.new(url: options.redis_url)
sentinels = options.sentinels
if sentinels
redis = ::Redis.new(url: options.redis_url, sentinels: sentinels)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if sentinels is just an empty array? Does Redis ignore that option? Is it possible to simply default the value to an empty array if no option is set (and thus avoid the conditional and local variable)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blargh. Okay, thanks.


context 'when sentinel is present' do
let(:redis_url) { 'redis://:mypassword@sentinel-master:6379' }
let(:sentinels) { [host: '10.0.0.1', port: '26379'] }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this configuration intended to be an array of hashes? Could we make that more explicit (adding {} or an additional sentinel config would make it more clear)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brodock Doesn't look done :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

o.0 ohhh i fixed the other spec that is similar, will commit to this one.

)
}

before { ::Redis::Client::Connector::Sentinel.any_instance.stubs(:resolve).returns(sentinels) }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This any_instance stubbing worries me, just a little. Combined with the instance_variable_get in the test has me curious.

Copy link
Contributor Author

@brodock brodock Oct 20, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's really hard to make any kind of test/mock for sentinel... I've opted here for checking library API contract, but I'm open to any idea you can have. (this is very far from a full integration test)

Copy link
Contributor Author

@brodock brodock Oct 20, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've opened an issue to have RedisMock available in the main Gem or into a separated Gem: redis/redis-rb#641

This would allow us to make a better test.

@tpitale
Copy link
Owner

tpitale commented Oct 20, 2016

Thanks for your work @brodock!

@tpitale
Copy link
Owner

tpitale commented Oct 21, 2016

@DouweM anything you'd like to add?

if sentinels
redis = ::Redis.new(url: options.redis_url, sentinels: sentinels)
else
redis = ::Redis.new(url: options.redis_url)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like repeating ::Redis.new(url: options.redis_url). What do you think about:

redis_options = {}
redis_options[:url] = options.redis_url
redis_options[:sentinels] = options.sentinels if options.sentinels
::Redis.new(redis_options)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or (one line shorter 😸 ):

redis_options = {url: options.redis_url}
redis_options[:sentinels] = options.sentinels if options.sentinels
::Redis.new(redis_options)

client = Redis.new(url: options.redis_url)
sentinels = options.sentinels
if sentinels
client = Redis.new(url: options.redis_url, sentinels: sentinels)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tpitale
Copy link
Owner

tpitale commented Oct 21, 2016

Travis failed due to GH outage. Will retest in a bit.

@tpitale
Copy link
Owner

tpitale commented Oct 21, 2016

Okay, now it's failing for some specific reason:

 1) MailRoom::Delivery::Sidekiq#options when only redis_url is specified client has same specified redis_url
     Failure/Error: return nil, nil unless ui
     SystemStackError:
       stack level too deep
     # /home/travis/.rvm/rubies/ruby-2.0.0-p598/lib/ruby/2.0.0/uri/generic.rb:557
     # 
     #   Showing full backtrace because every line was filtered out.
     #   See docs for RSpec::Configuration#backtrace_exclusion_patterns and
     #   RSpec::Configuration#backtrace_inclusion_patterns for more information.

@brodock
Copy link
Contributor Author

brodock commented Oct 24, 2016

@tpitale it's fixed.

@tpitale
Copy link
Owner

tpitale commented Oct 24, 2016

@brodock ready for me to merge?

@brodock
Copy link
Contributor Author

brodock commented Oct 24, 2016

@tpitale yes :)

@tpitale tpitale merged commit 4e6016c into tpitale:master Oct 25, 2016
@tpitale
Copy link
Owner

tpitale commented Oct 25, 2016

Released v0.9.0. Thanks again @brodock.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Redis sentinel support
4 participants