redis_failover 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Changes.md +11 -2
 - data/README.md +24 -5
 - data/lib/redis_failover.rb +1 -0
 - data/lib/redis_failover/cli.rb +1 -1
 - data/lib/redis_failover/client.rb +35 -31
 - data/lib/redis_failover/errors.rb +14 -3
 - data/lib/redis_failover/node.rb +65 -27
 - data/lib/redis_failover/node_manager.rb +53 -17
 - data/lib/redis_failover/node_watcher.rb +28 -13
 - data/lib/redis_failover/version.rb +1 -1
 - data/spec/client_spec.rb +10 -6
 - data/spec/node_manager_spec.rb +49 -16
 - data/spec/node_spec.rb +18 -10
 - data/spec/node_watcher_spec.rb +30 -15
 - data/spec/support/node_manager_stub.rb +15 -10
 - data/spec/support/redis_stub.rb +26 -11
 - metadata +16 -16
 
    
        data/Changes.md
    CHANGED
    
    | 
         @@ -1,8 +1,17 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            0.4.0
         
     | 
| 
      
 2 
     | 
    
         
            +
            -----------
         
     | 
| 
      
 3 
     | 
    
         
            +
            - No longer force newly available slaves to master if already slaves of that master
         
     | 
| 
      
 4 
     | 
    
         
            +
            - Honor a node's slave-serve-stale-data configuration option; do not mark a sync-with-master-in-progress slave as available if its slave-serve-stale-data is disabled
         
     | 
| 
      
 5 
     | 
    
         
            +
            - Change reachable/unreachable wording to available/unavailable
         
     | 
| 
      
 6 
     | 
    
         
            +
            - Added node reconciliation, i.e. if a node comes back up, make sure that the node manager and the node agree on current role
         
     | 
| 
      
 7 
     | 
    
         
            +
            - More efficient use of redis client connections
         
     | 
| 
      
 8 
     | 
    
         
            +
            - Raise proper error for unsupported operations (i.e., those that don't make sense for a failover client)
         
     | 
| 
      
 9 
     | 
    
         
            +
            - Properly handle any hanging node operations in the failover server
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
       1 
11 
     | 
    
         
             
            0.3.0
         
     | 
| 
       2 
12 
     | 
    
         
             
            -----------
         
     | 
| 
       3 
13 
     | 
    
         
             
            - Integrated travis-ci
         
     | 
| 
       4 
     | 
    
         
            -
            - Added background monitor to client for proactively detecting
         
     | 
| 
       5 
     | 
    
         
            -
              changes to current set of redis nodes
         
     | 
| 
      
 14 
     | 
    
         
            +
            - Added background monitor to client for proactively detecting changes to current set of redis nodes
         
     | 
| 
       6 
15 
     | 
    
         | 
| 
       7 
16 
     | 
    
         
             
            0.2.0
         
     | 
| 
       8 
17 
     | 
    
         
             
            -----------
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -13,15 +13,15 @@ master, and all reads go to one of the N configured slaves. 
     | 
|
| 
       13 
13 
     | 
    
         
             
            This gem attempts to address both the server and client problems. A redis failover server runs as a background
         
     | 
| 
       14 
14 
     | 
    
         
             
            daemon and monitors all of your configured master/slave nodes. When the server starts up, it
         
     | 
| 
       15 
15 
     | 
    
         
             
            automatically discovers who is the master and who are the slaves. Watchers are setup for each of
         
     | 
| 
       16 
     | 
    
         
            -
            the redis nodes. As soon as a node is detected as being offline, it will be moved to an " 
     | 
| 
      
 16 
     | 
    
         
            +
            the redis nodes. As soon as a node is detected as being offline, it will be moved to an "unavailable" state.
         
     | 
| 
       17 
17 
     | 
    
         
             
            If the node that went offline was the master, then one of the slaves will be promoted as the new master.
         
     | 
| 
       18 
18 
     | 
    
         
             
            All existing slaves will be automatically reconfigured to point to the new master for replication.
         
     | 
| 
       19 
     | 
    
         
            -
            All nodes marked as  
     | 
| 
       20 
     | 
    
         
            -
            If so, the newly  
     | 
| 
      
 19 
     | 
    
         
            +
            All nodes marked as unavailable will be periodically checked to see if they have been brought back online.
         
     | 
| 
      
 20 
     | 
    
         
            +
            If so, the newly available nodes will be configured as slaves and brought back into the list of live
         
     | 
| 
       21 
21 
     | 
    
         
             
            servers. Note that detection of a node going down should be nearly instantaneous, since the mechanism
         
     | 
| 
       22 
22 
     | 
    
         
             
            used to keep tabs on a node is via a blocking Redis BLPOP call (no polling). This call fails nearly
         
     | 
| 
       23 
23 
     | 
    
         
             
            immediately when the node actually goes offline. To avoid false positives (i.e., intermittent flaky
         
     | 
| 
       24 
     | 
    
         
            -
            network interruption), the server will only mark a node as  
     | 
| 
      
 24 
     | 
    
         
            +
            network interruption), the server will only mark a node as unavailable if it fails to communicate with
         
     | 
| 
       25 
25 
     | 
    
         
             
            it 3 times (this is configurable via --max-failures, see configuration options below).
         
     | 
| 
       26 
26 
     | 
    
         | 
| 
       27 
27 
     | 
    
         
             
            This gem provides a RedisFailover::Client wrapper that is master/slave aware. The client is configured
         
     | 
| 
         @@ -55,7 +55,7 @@ following options: 
     | 
|
| 
       55 
55 
     | 
    
         
             
                    -P, --port port                  Server port
         
     | 
| 
       56 
56 
     | 
    
         
             
                    -p, --password password          Redis password
         
     | 
| 
       57 
57 
     | 
    
         
             
                    -n, --nodes nodes                Comma-separated redis host:port pairs
         
     | 
| 
       58 
     | 
    
         
            -
                        --max-failures count         Max failures before server marks node  
     | 
| 
      
 58 
     | 
    
         
            +
                        --max-failures count         Max failures before server marks node unavailable (default 3)
         
     | 
| 
       59 
59 
     | 
    
         
             
                    -h, --help                       Display all options
         
     | 
| 
       60 
60 
     | 
    
         | 
| 
       61 
61 
     | 
    
         
             
            To start the server for a simple master/slave configuration, use the following:
         
     | 
| 
         @@ -87,6 +87,25 @@ The full set of options that can be passed to RedisFailover::Client are: 
     | 
|
| 
       87 
87 
     | 
    
         
             
                 :retry_failure - indicate if failures should be retried (default true)
         
     | 
| 
       88 
88 
     | 
    
         
             
                 :max_retries   - max retries for a failure (default 3)
         
     | 
| 
       89 
89 
     | 
    
         | 
| 
      
 90 
     | 
    
         
            +
            ## Requirements
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
            redis_failover is actively tested against MRI 1.9.2/1.9.3. Other rubies may work, although I don't actively test against them. 1.8 is not supported.
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
            ## Considerations
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
            Note that by default the failover server will mark slaves that are currently syncing with their master as "available" based on the configuration value set for "slave-serve-stale-data" in redis.conf. By default this value is set to "yes" in the configuration, which means that slaves still syncing with their master will be available for servicing read requests. If you don't want this behavior, just set "slave-serve-stale-data" to "no" in your redis.conf file.
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
            ## Limitations
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
            The redis_failover gem currently has limitations. It currently does not gracefully handle network partitions. In cases where
         
     | 
| 
      
 101 
     | 
    
         
            +
            the network splits, it is possible that more than one master could exist until the failover server sees all of the nodes again.
         
     | 
| 
      
 102 
     | 
    
         
            +
            If the failover client gets split from the failover server, it's also possible that it could be talking to a stale master. This would get corrected once the client could successfully reach the failover server again to fetch the latest set of master/slave nodes. This is a limitation that I hope to address in a future release. The gem can not guarantee data consistencies until this is addressed.
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
            ## TODO
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
            - Integrate with ZooKeeper for full reliability / data consistency.
         
     | 
| 
      
 107 
     | 
    
         
            +
            - Rework specs to work against a set of real Redis nodes as opposed to stubs.
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
       90 
109 
     | 
    
         
             
            ## Resources
         
     | 
| 
       91 
110 
     | 
    
         | 
| 
       92 
111 
     | 
    
         
             
            To learn more about Redis master/slave replication, see the [Redis documentation](http://redis.io/topics/replication).
         
     | 
    
        data/lib/redis_failover.rb
    CHANGED
    
    
    
        data/lib/redis_failover/cli.rb
    CHANGED
    
    | 
         @@ -24,7 +24,7 @@ module RedisFailover 
     | 
|
| 
       24 
24 
     | 
    
         
             
                    end
         
     | 
| 
       25 
25 
     | 
    
         | 
| 
       26 
26 
     | 
    
         
             
                    opts.on('--max-failures count',
         
     | 
| 
       27 
     | 
    
         
            -
                      'Max failures before server marks node  
     | 
| 
      
 27 
     | 
    
         
            +
                      'Max failures before server marks node unavailable (default 3)') do |max|
         
     | 
| 
       28 
28 
     | 
    
         
             
                      options[:max_failures] = Integer(max)
         
     | 
| 
       29 
29 
     | 
    
         
             
                    end
         
     | 
| 
       30 
30 
     | 
    
         | 
| 
         @@ -9,7 +9,6 @@ module RedisFailover 
     | 
|
| 
       9 
9 
     | 
    
         
             
                RETRY_WAIT_TIME = 3
         
     | 
| 
       10 
10 
     | 
    
         
             
                REDIS_ERRORS = Errno.constants.map { |c| Errno.const_get(c) }.freeze
         
     | 
| 
       11 
11 
     | 
    
         
             
                REDIS_READ_OPS = Set[
         
     | 
| 
       12 
     | 
    
         
            -
                  :dbsize,
         
     | 
| 
       13 
12 
     | 
    
         
             
                  :echo,
         
     | 
| 
       14 
13 
     | 
    
         
             
                  :exists,
         
     | 
| 
       15 
14 
     | 
    
         
             
                  :get,
         
     | 
| 
         @@ -22,26 +21,21 @@ module RedisFailover 
     | 
|
| 
       22 
21 
     | 
    
         
             
                  :hlen,
         
     | 
| 
       23 
22 
     | 
    
         
             
                  :hmget,
         
     | 
| 
       24 
23 
     | 
    
         
             
                  :hvals,
         
     | 
| 
       25 
     | 
    
         
            -
                  :info,
         
     | 
| 
       26 
24 
     | 
    
         
             
                  :keys,
         
     | 
| 
       27 
     | 
    
         
            -
                  :lastsave,
         
     | 
| 
       28 
25 
     | 
    
         
             
                  :lindex,
         
     | 
| 
       29 
26 
     | 
    
         
             
                  :llen,
         
     | 
| 
       30 
27 
     | 
    
         
             
                  :lrange,
         
     | 
| 
       31 
28 
     | 
    
         
             
                  :mapped_hmget,
         
     | 
| 
       32 
29 
     | 
    
         
             
                  :mapped_mget,
         
     | 
| 
       33 
30 
     | 
    
         
             
                  :mget,
         
     | 
| 
       34 
     | 
    
         
            -
                  :ping,
         
     | 
| 
       35 
31 
     | 
    
         
             
                  :scard,
         
     | 
| 
       36 
32 
     | 
    
         
             
                  :sdiff,
         
     | 
| 
       37 
     | 
    
         
            -
                  :select,
         
     | 
| 
       38 
33 
     | 
    
         
             
                  :sinter,
         
     | 
| 
       39 
34 
     | 
    
         
             
                  :sismember,
         
     | 
| 
       40 
35 
     | 
    
         
             
                  :smembers,
         
     | 
| 
       41 
36 
     | 
    
         
             
                  :srandmember,
         
     | 
| 
       42 
37 
     | 
    
         
             
                  :strlen,
         
     | 
| 
       43 
38 
     | 
    
         
             
                  :sunion,
         
     | 
| 
       44 
     | 
    
         
            -
                  :ttl,
         
     | 
| 
       45 
39 
     | 
    
         
             
                  :type,
         
     | 
| 
       46 
40 
     | 
    
         
             
                  :zcard,
         
     | 
| 
       47 
41 
     | 
    
         
             
                  :zcount,
         
     | 
| 
         @@ -54,6 +48,12 @@ module RedisFailover 
     | 
|
| 
       54 
48 
     | 
    
         
             
                  :zscore
         
     | 
| 
       55 
49 
     | 
    
         
             
                ].freeze
         
     | 
| 
       56 
50 
     | 
    
         | 
| 
      
 51 
     | 
    
         
            +
                UNSUPPORTED_OPS = Set[
         
     | 
| 
      
 52 
     | 
    
         
            +
                  :select,
         
     | 
| 
      
 53 
     | 
    
         
            +
                  :ttl,
         
     | 
| 
      
 54 
     | 
    
         
            +
                  :dbsize,
         
     | 
| 
      
 55 
     | 
    
         
            +
                ].freeze
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
       57 
57 
     | 
    
         
             
                # Performance optimization: to avoid unnecessary method_missing calls,
         
     | 
| 
       58 
58 
     | 
    
         
             
                # we proactively define methods that dispatch to the underlying redis
         
     | 
| 
       59 
59 
     | 
    
         
             
                # calls.
         
     | 
| 
         @@ -88,7 +88,6 @@ module RedisFailover 
     | 
|
| 
       88 
88 
     | 
    
         
             
                  @server_url = "http://#{options[:host]}:#{options[:port]}/redis_servers"
         
     | 
| 
       89 
89 
     | 
    
         
             
                  @master = nil
         
     | 
| 
       90 
90 
     | 
    
         
             
                  @slaves = []
         
     | 
| 
       91 
     | 
    
         
            -
                  @lock = Mutex.new
         
     | 
| 
       92 
91 
     | 
    
         
             
                  build_clients
         
     | 
| 
       93 
92 
     | 
    
         
             
                  start_background_monitor
         
     | 
| 
       94 
93 
     | 
    
         
             
                end
         
     | 
| 
         @@ -117,6 +116,7 @@ module RedisFailover 
     | 
|
| 
       117 
116 
     | 
    
         
             
                end
         
     | 
| 
       118 
117 
     | 
    
         | 
| 
       119 
118 
     | 
    
         
             
                def dispatch(method, *args, &block)
         
     | 
| 
      
 119 
     | 
    
         
            +
                  verify_supported!(method)
         
     | 
| 
       120 
120 
     | 
    
         
             
                  tries = 0
         
     | 
| 
       121 
121 
     | 
    
         | 
| 
       122 
122 
     | 
    
         
             
                  begin
         
     | 
| 
         @@ -159,32 +159,30 @@ module RedisFailover 
     | 
|
| 
       159 
159 
     | 
    
         
             
                end
         
     | 
| 
       160 
160 
     | 
    
         | 
| 
       161 
161 
     | 
    
         
             
                def build_clients
         
     | 
| 
       162 
     | 
    
         
            -
                   
     | 
| 
       163 
     | 
    
         
            -
                    tries = 0
         
     | 
| 
       164 
     | 
    
         
            -
             
     | 
| 
       165 
     | 
    
         
            -
                    begin
         
     | 
| 
       166 
     | 
    
         
            -
                      logger.info('Checking for new redis nodes.')
         
     | 
| 
       167 
     | 
    
         
            -
                      nodes = fetch_nodes
         
     | 
| 
       168 
     | 
    
         
            -
                      return unless nodes_changed?(nodes)
         
     | 
| 
       169 
     | 
    
         
            -
             
     | 
| 
       170 
     | 
    
         
            -
                      logger.info('Node change detected, rebuilding clients.')
         
     | 
| 
       171 
     | 
    
         
            -
                      master = new_clients_for(nodes[:master]).first if nodes[:master]
         
     | 
| 
       172 
     | 
    
         
            -
                      slaves = new_clients_for(*nodes[:slaves])
         
     | 
| 
       173 
     | 
    
         
            -
             
     | 
| 
       174 
     | 
    
         
            -
                      # once clients are successfully created, swap the references
         
     | 
| 
       175 
     | 
    
         
            -
                      @master = master
         
     | 
| 
       176 
     | 
    
         
            -
                      @slaves = slaves
         
     | 
| 
       177 
     | 
    
         
            -
                    rescue => ex
         
     | 
| 
       178 
     | 
    
         
            -
                      logger.error("Failed to fetch nodes from #{@server_url} - #{ex.message}")
         
     | 
| 
       179 
     | 
    
         
            -
                      logger.error(ex.backtrace.join("\n"))
         
     | 
| 
       180 
     | 
    
         
            -
             
     | 
| 
       181 
     | 
    
         
            -
                      if tries < @max_retries
         
     | 
| 
       182 
     | 
    
         
            -
                        tries += 1
         
     | 
| 
       183 
     | 
    
         
            -
                        sleep(RETRY_WAIT_TIME) && retry
         
     | 
| 
       184 
     | 
    
         
            -
                      end
         
     | 
| 
      
 162 
     | 
    
         
            +
                  tries = 0
         
     | 
| 
       185 
163 
     | 
    
         | 
| 
       186 
     | 
    
         
            -
             
     | 
| 
      
 164 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 165 
     | 
    
         
            +
                    logger.info('Checking for new redis nodes.')
         
     | 
| 
      
 166 
     | 
    
         
            +
                    nodes = fetch_nodes
         
     | 
| 
      
 167 
     | 
    
         
            +
                    return unless nodes_changed?(nodes)
         
     | 
| 
      
 168 
     | 
    
         
            +
             
     | 
| 
      
 169 
     | 
    
         
            +
                    logger.info('Node change detected, rebuilding clients.')
         
     | 
| 
      
 170 
     | 
    
         
            +
                    master = new_clients_for(nodes[:master]).first if nodes[:master]
         
     | 
| 
      
 171 
     | 
    
         
            +
                    slaves = new_clients_for(*nodes[:slaves])
         
     | 
| 
      
 172 
     | 
    
         
            +
             
     | 
| 
      
 173 
     | 
    
         
            +
                    # once clients are successfully created, swap the references
         
     | 
| 
      
 174 
     | 
    
         
            +
                    @master = master
         
     | 
| 
      
 175 
     | 
    
         
            +
                    @slaves = slaves
         
     | 
| 
      
 176 
     | 
    
         
            +
                  rescue => ex
         
     | 
| 
      
 177 
     | 
    
         
            +
                    logger.error("Failed to fetch nodes from #{@server_url} - #{ex.message}")
         
     | 
| 
      
 178 
     | 
    
         
            +
                    logger.error(ex.backtrace.join("\n"))
         
     | 
| 
      
 179 
     | 
    
         
            +
             
     | 
| 
      
 180 
     | 
    
         
            +
                    if tries < @max_retries
         
     | 
| 
      
 181 
     | 
    
         
            +
                      tries += 1
         
     | 
| 
      
 182 
     | 
    
         
            +
                      sleep(RETRY_WAIT_TIME) && retry
         
     | 
| 
       187 
183 
     | 
    
         
             
                    end
         
     | 
| 
      
 184 
     | 
    
         
            +
             
     | 
| 
      
 185 
     | 
    
         
            +
                    raise FailoverServerUnavailableError.new(@server_url)
         
     | 
| 
       188 
186 
     | 
    
         
             
                  end
         
     | 
| 
       189 
187 
     | 
    
         
             
                end
         
     | 
| 
       190 
188 
     | 
    
         | 
| 
         @@ -224,6 +222,12 @@ module RedisFailover 
     | 
|
| 
       224 
222 
     | 
    
         
             
                  role
         
     | 
| 
       225 
223 
     | 
    
         
             
                end
         
     | 
| 
       226 
224 
     | 
    
         | 
| 
      
 225 
     | 
    
         
            +
                def verify_supported!(method)
         
     | 
| 
      
 226 
     | 
    
         
            +
                  if UNSUPPORTED_OPS.include?(method)
         
     | 
| 
      
 227 
     | 
    
         
            +
                    raise UnsupportedOperationError.new(method)
         
     | 
| 
      
 228 
     | 
    
         
            +
                  end
         
     | 
| 
      
 229 
     | 
    
         
            +
                end
         
     | 
| 
      
 230 
     | 
    
         
            +
             
     | 
| 
       227 
231 
     | 
    
         
             
                def addresses_for(nodes)
         
     | 
| 
       228 
232 
     | 
    
         
             
                  nodes.map { |node| address_for(node) }
         
     | 
| 
       229 
233 
     | 
    
         
             
                end
         
     | 
| 
         @@ -1,5 +1,10 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module RedisFailover
         
     | 
| 
       2 
2 
     | 
    
         
             
              class Error < StandardError
         
     | 
| 
      
 3 
     | 
    
         
            +
                attr_reader :original
         
     | 
| 
      
 4 
     | 
    
         
            +
                def initialize(msg = nil, original = $!)
         
     | 
| 
      
 5 
     | 
    
         
            +
                  super(msg)
         
     | 
| 
      
 6 
     | 
    
         
            +
                  @original = original
         
     | 
| 
      
 7 
     | 
    
         
            +
                end
         
     | 
| 
       3 
8 
     | 
    
         
             
              end
         
     | 
| 
       4 
9 
     | 
    
         | 
| 
       5 
10 
     | 
    
         
             
              class InvalidNodeError < Error
         
     | 
| 
         @@ -11,7 +16,7 @@ module RedisFailover 
     | 
|
| 
       11 
16 
     | 
    
         
             
                end
         
     | 
| 
       12 
17 
     | 
    
         
             
              end
         
     | 
| 
       13 
18 
     | 
    
         | 
| 
       14 
     | 
    
         
            -
              class  
     | 
| 
      
 19 
     | 
    
         
            +
              class NodeUnavailableError < Error
         
     | 
| 
       15 
20 
     | 
    
         
             
                def initialize(node)
         
     | 
| 
       16 
21 
     | 
    
         
             
                  super("Node: #{node}")
         
     | 
| 
       17 
22 
     | 
    
         
             
                end
         
     | 
| 
         @@ -23,9 +28,9 @@ module RedisFailover 
     | 
|
| 
       23 
28 
     | 
    
         
             
              class NoSlaveError < Error
         
     | 
| 
       24 
29 
     | 
    
         
             
              end
         
     | 
| 
       25 
30 
     | 
    
         | 
| 
       26 
     | 
    
         
            -
              class  
     | 
| 
      
 31 
     | 
    
         
            +
              class FailoverServerUnavailableError < Error
         
     | 
| 
       27 
32 
     | 
    
         
             
                def initialize(failover_server_url)
         
     | 
| 
       28 
     | 
    
         
            -
                  super("Unable to  
     | 
| 
      
 33 
     | 
    
         
            +
                  super("Unable to access #{failover_server_url}")
         
     | 
| 
       29 
34 
     | 
    
         
             
                end
         
     | 
| 
       30 
35 
     | 
    
         
             
              end
         
     | 
| 
       31 
36 
     | 
    
         | 
| 
         @@ -35,4 +40,10 @@ module RedisFailover 
     | 
|
| 
       35 
40 
     | 
    
         
             
                    "it was a #{assumed}, but it's now a #{actual}")
         
     | 
| 
       36 
41 
     | 
    
         
             
                end
         
     | 
| 
       37 
42 
     | 
    
         
             
              end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
              class UnsupportedOperationError < Error
         
     | 
| 
      
 45 
     | 
    
         
            +
                def initialize(operation)
         
     | 
| 
      
 46 
     | 
    
         
            +
                  super("Operation `#{operation}` is currently unsupported")
         
     | 
| 
      
 47 
     | 
    
         
            +
                end
         
     | 
| 
      
 48 
     | 
    
         
            +
              end
         
     | 
| 
       38 
49 
     | 
    
         
             
            end
         
     | 
    
        data/lib/redis_failover/node.rb
    CHANGED
    
    | 
         @@ -3,6 +3,8 @@ module RedisFailover 
     | 
|
| 
       3 
3 
     | 
    
         
             
              class Node
         
     | 
| 
       4 
4 
     | 
    
         
             
                include Util
         
     | 
| 
       5 
5 
     | 
    
         | 
| 
      
 6 
     | 
    
         
            +
                MAX_OP_WAIT_TIME = 5
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
       6 
8 
     | 
    
         
             
                attr_reader :host, :port
         
     | 
| 
       7 
9 
     | 
    
         | 
| 
       8 
10 
     | 
    
         
             
                def initialize(options = {})
         
     | 
| 
         @@ -11,12 +13,6 @@ module RedisFailover 
     | 
|
| 
       11 
13 
     | 
    
         
             
                  @password = options[:password]
         
     | 
| 
       12 
14 
     | 
    
         
             
                end
         
     | 
| 
       13 
15 
     | 
    
         | 
| 
       14 
     | 
    
         
            -
                def ping
         
     | 
| 
       15 
     | 
    
         
            -
                  perform_operation do
         
     | 
| 
       16 
     | 
    
         
            -
                    redis.ping
         
     | 
| 
       17 
     | 
    
         
            -
                  end
         
     | 
| 
       18 
     | 
    
         
            -
                end
         
     | 
| 
       19 
     | 
    
         
            -
             
     | 
| 
       20 
16 
     | 
    
         
             
                def master?
         
     | 
| 
       21 
17 
     | 
    
         
             
                  role == 'master'
         
     | 
| 
       22 
18 
     | 
    
         
             
                end
         
     | 
| 
         @@ -25,29 +21,48 @@ module RedisFailover 
     | 
|
| 
       25 
21 
     | 
    
         
             
                  !master?
         
     | 
| 
       26 
22 
     | 
    
         
             
                end
         
     | 
| 
       27 
23 
     | 
    
         | 
| 
       28 
     | 
    
         
            -
                def  
     | 
| 
       29 
     | 
    
         
            -
                   
     | 
| 
       30 
     | 
    
         
            -
             
     | 
| 
      
 24 
     | 
    
         
            +
                def slave_of?(master)
         
     | 
| 
      
 25 
     | 
    
         
            +
                  current_master == master
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                def current_master
         
     | 
| 
      
 29 
     | 
    
         
            +
                  info = fetch_info
         
     | 
| 
      
 30 
     | 
    
         
            +
                  return unless info[:role] == 'slave'
         
     | 
| 
      
 31 
     | 
    
         
            +
                  Node.new(:host => info[:master_host], :port => info[:master_port].to_i)
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                # Waits until something interesting happens. If the connection
         
     | 
| 
      
 35 
     | 
    
         
            +
                # with this node dies, the blpop call will raise an error. If
         
     | 
| 
      
 36 
     | 
    
         
            +
                # the blpop call returns without error, then this will be due to
         
     | 
| 
      
 37 
     | 
    
         
            +
                # a graceful shutdown signaled by #wakeup or a timeout.
         
     | 
| 
      
 38 
     | 
    
         
            +
                def wait
         
     | 
| 
      
 39 
     | 
    
         
            +
                  perform_operation do |redis|
         
     | 
| 
      
 40 
     | 
    
         
            +
                    redis.blpop(wait_key, MAX_OP_WAIT_TIME - 3)
         
     | 
| 
       31 
41 
     | 
    
         
             
                    redis.del(wait_key)
         
     | 
| 
       32 
42 
     | 
    
         
             
                  end
         
     | 
| 
       33 
43 
     | 
    
         
             
                end
         
     | 
| 
       34 
44 
     | 
    
         | 
| 
       35 
     | 
    
         
            -
                def  
     | 
| 
       36 
     | 
    
         
            -
                  perform_operation do
         
     | 
| 
      
 45 
     | 
    
         
            +
                def wakeup
         
     | 
| 
      
 46 
     | 
    
         
            +
                  perform_operation do |redis|
         
     | 
| 
       37 
47 
     | 
    
         
             
                    redis.lpush(wait_key, '1')
         
     | 
| 
       38 
48 
     | 
    
         
             
                  end
         
     | 
| 
       39 
49 
     | 
    
         
             
                end
         
     | 
| 
       40 
50 
     | 
    
         | 
| 
       41 
51 
     | 
    
         
             
                def make_slave!(master)
         
     | 
| 
       42 
     | 
    
         
            -
                  perform_operation do
         
     | 
| 
       43 
     | 
    
         
            -
                     
     | 
| 
      
 52 
     | 
    
         
            +
                  perform_operation do |redis|
         
     | 
| 
      
 53 
     | 
    
         
            +
                    unless slave_of?(master)
         
     | 
| 
      
 54 
     | 
    
         
            +
                      redis.slaveof(master.host, master.port)
         
     | 
| 
      
 55 
     | 
    
         
            +
                      wakeup
         
     | 
| 
      
 56 
     | 
    
         
            +
                    end
         
     | 
| 
       44 
57 
     | 
    
         
             
                  end
         
     | 
| 
       45 
58 
     | 
    
         
             
                end
         
     | 
| 
       46 
59 
     | 
    
         | 
| 
       47 
60 
     | 
    
         
             
                def make_master!
         
     | 
| 
       48 
     | 
    
         
            -
                  perform_operation do
         
     | 
| 
       49 
     | 
    
         
            -
                     
     | 
| 
       50 
     | 
    
         
            -
             
     | 
| 
      
 61 
     | 
    
         
            +
                  perform_operation do |redis|
         
     | 
| 
      
 62 
     | 
    
         
            +
                    unless master?
         
     | 
| 
      
 63 
     | 
    
         
            +
                      redis.slaveof('no', 'one')
         
     | 
| 
      
 64 
     | 
    
         
            +
                      wakeup
         
     | 
| 
      
 65 
     | 
    
         
            +
                    end
         
     | 
| 
       51 
66 
     | 
    
         
             
                  end
         
     | 
| 
       52 
67 
     | 
    
         
             
                end
         
     | 
| 
       53 
68 
     | 
    
         | 
| 
         @@ -70,32 +85,55 @@ module RedisFailover 
     | 
|
| 
       70 
85 
     | 
    
         
             
                  to_s.hash
         
     | 
| 
       71 
86 
     | 
    
         
             
                end
         
     | 
| 
       72 
87 
     | 
    
         | 
| 
       73 
     | 
    
         
            -
                 
     | 
| 
      
 88 
     | 
    
         
            +
                def fetch_info
         
     | 
| 
      
 89 
     | 
    
         
            +
                  perform_operation do |redis|
         
     | 
| 
      
 90 
     | 
    
         
            +
                    symbolize_keys(redis.info)
         
     | 
| 
      
 91 
     | 
    
         
            +
                  end
         
     | 
| 
      
 92 
     | 
    
         
            +
                end
         
     | 
| 
      
 93 
     | 
    
         
            +
                alias_method :ping, :fetch_info
         
     | 
| 
       74 
94 
     | 
    
         | 
| 
       75 
     | 
    
         
            -
                def  
     | 
| 
       76 
     | 
    
         
            -
                   
     | 
| 
      
 95 
     | 
    
         
            +
                def prohibits_stale_reads?
         
     | 
| 
      
 96 
     | 
    
         
            +
                  perform_operation do |redis|
         
     | 
| 
      
 97 
     | 
    
         
            +
                    redis.config('get', 'slave-serve-stale-data').last == 'no'
         
     | 
| 
      
 98 
     | 
    
         
            +
                  end
         
     | 
| 
       77 
99 
     | 
    
         
             
                end
         
     | 
| 
       78 
100 
     | 
    
         | 
| 
       79 
     | 
    
         
            -
                def  
     | 
| 
       80 
     | 
    
         
            -
                  perform_operation do
         
     | 
| 
       81 
     | 
    
         
            -
                     
     | 
| 
      
 101 
     | 
    
         
            +
                def syncing_with_master?
         
     | 
| 
      
 102 
     | 
    
         
            +
                  perform_operation do |redis|
         
     | 
| 
      
 103 
     | 
    
         
            +
                    fetch_info[:master_sync_in_progress] == '1'
         
     | 
| 
       82 
104 
     | 
    
         
             
                  end
         
     | 
| 
       83 
105 
     | 
    
         
             
                end
         
     | 
| 
       84 
106 
     | 
    
         | 
| 
      
 107 
     | 
    
         
            +
                private
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                def role
         
     | 
| 
      
 110 
     | 
    
         
            +
                  fetch_info[:role]
         
     | 
| 
      
 111 
     | 
    
         
            +
                end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
       85 
113 
     | 
    
         
             
                def wait_key
         
     | 
| 
       86 
114 
     | 
    
         
             
                  @wait_key ||= "_redis_failover_#{SecureRandom.hex(32)}"
         
     | 
| 
       87 
115 
     | 
    
         
             
                end
         
     | 
| 
       88 
116 
     | 
    
         | 
| 
       89 
     | 
    
         
            -
                def  
     | 
| 
      
 117 
     | 
    
         
            +
                def new_client
         
     | 
| 
       90 
118 
     | 
    
         
             
                  Redis.new(:host => @host, :password => @password, :port => @port)
         
     | 
| 
       91 
     | 
    
         
            -
                rescue
         
     | 
| 
       92 
     | 
    
         
            -
                  raise NodeUnreachableError.new(self)
         
     | 
| 
       93 
119 
     | 
    
         
             
                end
         
     | 
| 
       94 
120 
     | 
    
         | 
| 
       95 
121 
     | 
    
         
             
                def perform_operation
         
     | 
| 
       96 
     | 
    
         
            -
                   
     | 
| 
      
 122 
     | 
    
         
            +
                  redis = nil
         
     | 
| 
      
 123 
     | 
    
         
            +
                  Timeout.timeout(MAX_OP_WAIT_TIME) do
         
     | 
| 
      
 124 
     | 
    
         
            +
                    redis = new_client
         
     | 
| 
      
 125 
     | 
    
         
            +
                    yield redis
         
     | 
| 
      
 126 
     | 
    
         
            +
                  end
         
     | 
| 
       97 
127 
     | 
    
         
             
                rescue
         
     | 
| 
       98 
     | 
    
         
            -
                  raise  
     | 
| 
      
 128 
     | 
    
         
            +
                  raise NodeUnavailableError.new(self)
         
     | 
| 
      
 129 
     | 
    
         
            +
                ensure
         
     | 
| 
      
 130 
     | 
    
         
            +
                  if redis
         
     | 
| 
      
 131 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 132 
     | 
    
         
            +
                      redis.client.disconnect
         
     | 
| 
      
 133 
     | 
    
         
            +
                    rescue
         
     | 
| 
      
 134 
     | 
    
         
            +
                      raise NodeUnavailableError.new(self)
         
     | 
| 
      
 135 
     | 
    
         
            +
                    end
         
     | 
| 
      
 136 
     | 
    
         
            +
                  end
         
     | 
| 
       99 
137 
     | 
    
         
             
                end
         
     | 
| 
       100 
138 
     | 
    
         
             
              end
         
     | 
| 
       101 
139 
     | 
    
         
             
            end
         
     | 
| 
         @@ -6,7 +6,7 @@ module RedisFailover 
     | 
|
| 
       6 
6 
     | 
    
         
             
                def initialize(options)
         
     | 
| 
       7 
7 
     | 
    
         
             
                  @options = options
         
     | 
| 
       8 
8 
     | 
    
         
             
                  @master, @slaves = parse_nodes
         
     | 
| 
       9 
     | 
    
         
            -
                  @ 
     | 
| 
      
 9 
     | 
    
         
            +
                  @unavailable = []
         
     | 
| 
       10 
10 
     | 
    
         
             
                  @queue = Queue.new
         
     | 
| 
       11 
11 
     | 
    
         
             
                  @lock = Mutex.new
         
     | 
| 
       12 
12 
     | 
    
         
             
                end
         
     | 
| 
         @@ -25,7 +25,7 @@ module RedisFailover 
     | 
|
| 
       25 
25 
     | 
    
         
             
                    {
         
     | 
| 
       26 
26 
     | 
    
         
             
                      :master => @master ? @master.to_s : nil,
         
     | 
| 
       27 
27 
     | 
    
         
             
                      :slaves => @slaves.map(&:to_s),
         
     | 
| 
       28 
     | 
    
         
            -
                      : 
     | 
| 
      
 28 
     | 
    
         
            +
                      :unavailable => @unavailable.map(&:to_s)
         
     | 
| 
       29 
29 
     | 
    
         
             
                    }
         
     | 
| 
       30 
30 
     | 
    
         
             
                  end
         
     | 
| 
       31 
31 
     | 
    
         
             
                end
         
     | 
| 
         @@ -42,12 +42,13 @@ module RedisFailover 
     | 
|
| 
       42 
42 
     | 
    
         
             
                      node, state = state_change
         
     | 
| 
       43 
43 
     | 
    
         
             
                      begin
         
     | 
| 
       44 
44 
     | 
    
         
             
                        case state
         
     | 
| 
       45 
     | 
    
         
            -
                        when : 
     | 
| 
       46 
     | 
    
         
            -
                        when : 
     | 
| 
      
 45 
     | 
    
         
            +
                        when :unavailable then handle_unavailable(node)
         
     | 
| 
      
 46 
     | 
    
         
            +
                        when :available   then handle_available(node)
         
     | 
| 
      
 47 
     | 
    
         
            +
                        when :syncing     then handle_syncing(node)
         
     | 
| 
       47 
48 
     | 
    
         
             
                        else raise InvalidNodeStateError.new(node, state)
         
     | 
| 
       48 
49 
     | 
    
         
             
                        end
         
     | 
| 
       49 
     | 
    
         
            -
                      rescue  
     | 
| 
       50 
     | 
    
         
            -
                        # node suddenly became  
     | 
| 
      
 50 
     | 
    
         
            +
                      rescue NodeUnavailableError
         
     | 
| 
      
 51 
     | 
    
         
            +
                        # node suddenly became unavailable, silently
         
     | 
| 
       51 
52 
     | 
    
         
             
                        # handle since the watcher will take care of
         
     | 
| 
       52 
53 
     | 
    
         
             
                        # keeping track of the node
         
     | 
| 
       53 
54 
     | 
    
         
             
                      end
         
     | 
| 
         @@ -55,25 +56,27 @@ module RedisFailover 
     | 
|
| 
       55 
56 
     | 
    
         
             
                  end
         
     | 
| 
       56 
57 
     | 
    
         
             
                end
         
     | 
| 
       57 
58 
     | 
    
         | 
| 
       58 
     | 
    
         
            -
                def  
     | 
| 
      
 59 
     | 
    
         
            +
                def handle_unavailable(node)
         
     | 
| 
       59 
60 
     | 
    
         
             
                  # no-op if we already know about this node
         
     | 
| 
       60 
     | 
    
         
            -
                  return if @ 
     | 
| 
       61 
     | 
    
         
            -
                  logger.info("Handling  
     | 
| 
      
 61 
     | 
    
         
            +
                  return if @unavailable.include?(node)
         
     | 
| 
      
 62 
     | 
    
         
            +
                  logger.info("Handling unavailable node: #{node}")
         
     | 
| 
       62 
63 
     | 
    
         | 
| 
       63 
     | 
    
         
            -
                  @ 
     | 
| 
      
 64 
     | 
    
         
            +
                  @unavailable << node
         
     | 
| 
       64 
65 
     | 
    
         
             
                  # find a new master if this node was a master
         
     | 
| 
       65 
66 
     | 
    
         
             
                  if node == @master
         
     | 
| 
       66 
     | 
    
         
            -
                    logger.info("Demoting currently  
     | 
| 
      
 67 
     | 
    
         
            +
                    logger.info("Demoting currently unavailable master #{node}.")
         
     | 
| 
       67 
68 
     | 
    
         
             
                    promote_new_master
         
     | 
| 
       68 
69 
     | 
    
         
             
                  else
         
     | 
| 
       69 
70 
     | 
    
         
             
                    @slaves.delete(node)
         
     | 
| 
       70 
71 
     | 
    
         
             
                  end
         
     | 
| 
       71 
72 
     | 
    
         
             
                end
         
     | 
| 
       72 
73 
     | 
    
         | 
| 
       73 
     | 
    
         
            -
                def  
     | 
| 
      
 74 
     | 
    
         
            +
                def handle_available(node)
         
     | 
| 
      
 75 
     | 
    
         
            +
                  reconcile(node)
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
       74 
77 
     | 
    
         
             
                  # no-op if we already know about this node
         
     | 
| 
       75 
78 
     | 
    
         
             
                  return if @master == node || @slaves.include?(node)
         
     | 
| 
       76 
     | 
    
         
            -
                  logger.info("Handling  
     | 
| 
      
 79 
     | 
    
         
            +
                  logger.info("Handling available node: #{node}")
         
     | 
| 
       77 
80 
     | 
    
         | 
| 
       78 
81 
     | 
    
         
             
                  if @master
         
     | 
| 
       79 
82 
     | 
    
         
             
                    # master already exists, make a slave
         
     | 
| 
         @@ -84,7 +87,20 @@ module RedisFailover 
     | 
|
| 
       84 
87 
     | 
    
         
             
                    promote_new_master(node)
         
     | 
| 
       85 
88 
     | 
    
         
             
                  end
         
     | 
| 
       86 
89 
     | 
    
         | 
| 
       87 
     | 
    
         
            -
                  @ 
     | 
| 
      
 90 
     | 
    
         
            +
                  @unavailable.delete(node)
         
     | 
| 
      
 91 
     | 
    
         
            +
                end
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                def handle_syncing(node)
         
     | 
| 
      
 94 
     | 
    
         
            +
                  reconcile(node)
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
                  if node.prohibits_stale_reads?
         
     | 
| 
      
 97 
     | 
    
         
            +
                    logger.info("Node #{node} not ready yet, still syncing with master.")
         
     | 
| 
      
 98 
     | 
    
         
            +
                    @unavailable << node
         
     | 
| 
      
 99 
     | 
    
         
            +
                    return
         
     | 
| 
      
 100 
     | 
    
         
            +
                  end
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                  # otherwise, we can use this node
         
     | 
| 
      
 103 
     | 
    
         
            +
                  handle_available(node)
         
     | 
| 
       88 
104 
     | 
    
         
             
                end
         
     | 
| 
       89 
105 
     | 
    
         | 
| 
       90 
106 
     | 
    
         
             
                def promote_new_master(node = nil)
         
     | 
| 
         @@ -104,7 +120,7 @@ module RedisFailover 
     | 
|
| 
       104 
120 
     | 
    
         
             
                end
         
     | 
| 
       105 
121 
     | 
    
         | 
| 
       106 
122 
     | 
    
         
             
                def parse_nodes
         
     | 
| 
       107 
     | 
    
         
            -
                  nodes = @options[:nodes].map { |opts| Node.new(opts) }
         
     | 
| 
      
 123 
     | 
    
         
            +
                  nodes = @options[:nodes].map { |opts| Node.new(opts) }.uniq
         
     | 
| 
       108 
124 
     | 
    
         
             
                  raise NoMasterError unless master = find_master(nodes)
         
     | 
| 
       109 
125 
     | 
    
         
             
                  slaves = nodes - [master]
         
     | 
| 
       110 
126 
     | 
    
         | 
| 
         @@ -127,7 +143,7 @@ module RedisFailover 
     | 
|
| 
       127 
143 
     | 
    
         
             
                  nodes.find do |node|
         
     | 
| 
       128 
144 
     | 
    
         
             
                    begin
         
     | 
| 
       129 
145 
     | 
    
         
             
                      node.master?
         
     | 
| 
       130 
     | 
    
         
            -
                    rescue  
     | 
| 
      
 146 
     | 
    
         
            +
                    rescue NodeUnavailableError
         
     | 
| 
       131 
147 
     | 
    
         
             
                      # will eventually be handled by watcher
         
     | 
| 
       132 
148 
     | 
    
         
             
                      false
         
     | 
| 
       133 
149 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -139,10 +155,30 @@ module RedisFailover 
     | 
|
| 
       139 
155 
     | 
    
         
             
                  @slaves.each do |slave|
         
     | 
| 
       140 
156 
     | 
    
         
             
                    begin
         
     | 
| 
       141 
157 
     | 
    
         
             
                      slave.make_slave!(@master)
         
     | 
| 
       142 
     | 
    
         
            -
                    rescue  
     | 
| 
      
 158 
     | 
    
         
            +
                    rescue NodeUnavailableError
         
     | 
| 
       143 
159 
     | 
    
         
             
                      # will also be detected by watcher
         
     | 
| 
       144 
160 
     | 
    
         
             
                    end
         
     | 
| 
       145 
161 
     | 
    
         
             
                  end
         
     | 
| 
       146 
162 
     | 
    
         
             
                end
         
     | 
| 
      
 163 
     | 
    
         
            +
             
     | 
| 
      
 164 
     | 
    
         
            +
                # It's possible that a newly available node may have been restarted
         
     | 
| 
      
 165 
     | 
    
         
            +
                # and completely lost its dynamically set run-time role by the node
         
     | 
| 
      
 166 
     | 
    
         
            +
                # manager. This method ensures that the node resumes its role as
         
     | 
| 
      
 167 
     | 
    
         
            +
                # determined by the manager.
         
     | 
| 
      
 168 
     | 
    
         
            +
                def reconcile(node)
         
     | 
| 
      
 169 
     | 
    
         
            +
                  return if @master == node && node.master?
         
     | 
| 
      
 170 
     | 
    
         
            +
                  return if @master && node.slave_of?(@master)
         
     | 
| 
      
 171 
     | 
    
         
            +
             
     | 
| 
      
 172 
     | 
    
         
            +
                  if @master == node && !node.master?
         
     | 
| 
      
 173 
     | 
    
         
            +
                    # we think the node is a master, but the node doesn't
         
     | 
| 
      
 174 
     | 
    
         
            +
                    node.make_master!
         
     | 
| 
      
 175 
     | 
    
         
            +
                    return
         
     | 
| 
      
 176 
     | 
    
         
            +
                  end
         
     | 
| 
      
 177 
     | 
    
         
            +
             
     | 
| 
      
 178 
     | 
    
         
            +
                  # verify that node is a slave for the current master
         
     | 
| 
      
 179 
     | 
    
         
            +
                  if @master && !node.slave_of?(@master)
         
     | 
| 
      
 180 
     | 
    
         
            +
                    node.make_slave!(@master)
         
     | 
| 
      
 181 
     | 
    
         
            +
                  end
         
     | 
| 
      
 182 
     | 
    
         
            +
                end
         
     | 
| 
       147 
183 
     | 
    
         
             
              end
         
     | 
| 
       148 
184 
     | 
    
         
             
            end
         
     | 
| 
         @@ -1,6 +1,10 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module RedisFailover
         
     | 
| 
       2 
     | 
    
         
            -
              # Watches a specific redis node for its  
     | 
| 
      
 2 
     | 
    
         
            +
              # Watches a specific redis node for its availability.
         
     | 
| 
       3 
3 
     | 
    
         
             
              class NodeWatcher
         
     | 
| 
      
 4 
     | 
    
         
            +
                include Util
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                WATCHER_SLEEP_TIME = 3
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
       4 
8 
     | 
    
         
             
                def initialize(manager, node, max_failures)
         
     | 
| 
       5 
9 
     | 
    
         
             
                  @manager = manager
         
     | 
| 
       6 
10 
     | 
    
         
             
                  @node = node
         
     | 
| 
         @@ -16,7 +20,7 @@ module RedisFailover 
     | 
|
| 
       16 
20 
     | 
    
         | 
| 
       17 
21 
     | 
    
         
             
                def shutdown
         
     | 
| 
       18 
22 
     | 
    
         
             
                  @done = true
         
     | 
| 
       19 
     | 
    
         
            -
                  @node. 
     | 
| 
      
 23 
     | 
    
         
            +
                  @node.wakeup
         
     | 
| 
       20 
24 
     | 
    
         
             
                  @monitor_thread.join if @monitor_thread
         
     | 
| 
       21 
25 
     | 
    
         
             
                rescue
         
     | 
| 
       22 
26 
     | 
    
         
             
                  # best effort
         
     | 
| 
         @@ -27,20 +31,31 @@ module RedisFailover 
     | 
|
| 
       27 
31 
     | 
    
         
             
                def monitor_node
         
     | 
| 
       28 
32 
     | 
    
         
             
                  failures = 0
         
     | 
| 
       29 
33 
     | 
    
         | 
| 
       30 
     | 
    
         
            -
                   
     | 
| 
       31 
     | 
    
         
            -
                     
     | 
| 
       32 
     | 
    
         
            -
             
     | 
| 
       33 
     | 
    
         
            -
             
     | 
| 
       34 
     | 
    
         
            -
             
     | 
| 
       35 
     | 
    
         
            -
                    @node.wait_until_unreachable
         
     | 
| 
       36 
     | 
    
         
            -
                  rescue NodeUnreachableError
         
     | 
| 
       37 
     | 
    
         
            -
                    failures += 1
         
     | 
| 
       38 
     | 
    
         
            -
                    if failures >= @max_failures
         
     | 
| 
       39 
     | 
    
         
            -
                      @manager.notify_state_change(@node, :unreachable)
         
     | 
| 
      
 34 
     | 
    
         
            +
                  loop do
         
     | 
| 
      
 35 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 36 
     | 
    
         
            +
                      return if @done
         
     | 
| 
      
 37 
     | 
    
         
            +
                      sleep(WATCHER_SLEEP_TIME)
         
     | 
| 
      
 38 
     | 
    
         
            +
                      @node.ping
         
     | 
| 
       40 
39 
     | 
    
         
             
                      failures = 0
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                      if @node.syncing_with_master?
         
     | 
| 
      
 42 
     | 
    
         
            +
                        notify(:syncing)
         
     | 
| 
      
 43 
     | 
    
         
            +
                      else
         
     | 
| 
      
 44 
     | 
    
         
            +
                        notify(:available)
         
     | 
| 
      
 45 
     | 
    
         
            +
                        @node.wait
         
     | 
| 
      
 46 
     | 
    
         
            +
                      end
         
     | 
| 
      
 47 
     | 
    
         
            +
                    rescue NodeUnavailableError
         
     | 
| 
      
 48 
     | 
    
         
            +
                      failures += 1
         
     | 
| 
      
 49 
     | 
    
         
            +
                      if failures >= @max_failures
         
     | 
| 
      
 50 
     | 
    
         
            +
                        notify(:unavailable)
         
     | 
| 
      
 51 
     | 
    
         
            +
                        failures = 0
         
     | 
| 
      
 52 
     | 
    
         
            +
                      end
         
     | 
| 
       41 
53 
     | 
    
         
             
                    end
         
     | 
| 
       42 
     | 
    
         
            -
                    sleep(3) && retry
         
     | 
| 
       43 
54 
     | 
    
         
             
                  end
         
     | 
| 
       44 
55 
     | 
    
         
             
                end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                def notify(state)
         
     | 
| 
      
 58 
     | 
    
         
            +
                  @manager.notify_state_change(@node, state)
         
     | 
| 
      
 59 
     | 
    
         
            +
                end
         
     | 
| 
       45 
60 
     | 
    
         
             
              end
         
     | 
| 
       46 
61 
     | 
    
         
             
            end
         
     | 
    
        data/spec/client_spec.rb
    CHANGED
    
    | 
         @@ -15,7 +15,7 @@ module RedisFailover 
     | 
|
| 
       15 
15 
     | 
    
         
             
                  {
         
     | 
| 
       16 
16 
     | 
    
         
             
                    :master => 'localhost:6379',
         
     | 
| 
       17 
17 
     | 
    
         
             
                    :slaves => ['localhost:1111'],
         
     | 
| 
       18 
     | 
    
         
            -
                    : 
     | 
| 
      
 18 
     | 
    
         
            +
                    :unavailable => []
         
     | 
| 
       19 
19 
     | 
    
         
             
                  }
         
     | 
| 
       20 
20 
     | 
    
         
             
                end
         
     | 
| 
       21 
21 
     | 
    
         
             
              end
         
     | 
| 
         @@ -57,7 +57,7 @@ module RedisFailover 
     | 
|
| 
       57 
57 
     | 
    
         
             
                    client.get('foo')
         
     | 
| 
       58 
58 
     | 
    
         
             
                  end
         
     | 
| 
       59 
59 
     | 
    
         | 
| 
       60 
     | 
    
         
            -
                  it 'reconnects with redis failover server when node is  
     | 
| 
      
 60 
     | 
    
         
            +
                  it 'reconnects with redis failover server when node is unavailable' do
         
     | 
| 
       61 
61 
     | 
    
         
             
                    class << client
         
     | 
| 
       62 
62 
     | 
    
         
             
                      attr_reader :reconnected
         
     | 
| 
       63 
63 
     | 
    
         
             
                      def build_clients
         
     | 
| 
         @@ -70,26 +70,30 @@ module RedisFailover 
     | 
|
| 
       70 
70 
     | 
    
         
             
                        {
         
     | 
| 
       71 
71 
     | 
    
         
             
                          :master => "localhost:222#{@calls += 1}",
         
     | 
| 
       72 
72 
     | 
    
         
             
                          :slaves => ['localhost:1111'],
         
     | 
| 
       73 
     | 
    
         
            -
                          : 
     | 
| 
      
 73 
     | 
    
         
            +
                          :unavailable => []
         
     | 
| 
       74 
74 
     | 
    
         
             
                        }
         
     | 
| 
       75 
75 
     | 
    
         
             
                      end
         
     | 
| 
       76 
76 
     | 
    
         
             
                    end
         
     | 
| 
       77 
77 
     | 
    
         | 
| 
       78 
     | 
    
         
            -
                    client.current_master. 
     | 
| 
      
 78 
     | 
    
         
            +
                    client.current_master.make_unavailable!
         
     | 
| 
       79 
79 
     | 
    
         
             
                    client.del('foo')
         
     | 
| 
       80 
80 
     | 
    
         
             
                    client.reconnected.should be_true
         
     | 
| 
       81 
81 
     | 
    
         
             
                  end
         
     | 
| 
       82 
82 
     | 
    
         | 
| 
       83 
     | 
    
         
            -
                  it 'fails hard when the failover server is  
     | 
| 
      
 83 
     | 
    
         
            +
                  it 'fails hard when the failover server is unavailable' do
         
     | 
| 
       84 
84 
     | 
    
         
             
                    expect do
         
     | 
| 
       85 
85 
     | 
    
         
             
                      Client.new(:host => 'foo', :port => 123445)
         
     | 
| 
       86 
     | 
    
         
            -
                    end.to raise_error( 
     | 
| 
      
 86 
     | 
    
         
            +
                    end.to raise_error(FailoverServerUnavailableError)
         
     | 
| 
       87 
87 
     | 
    
         
             
                  end
         
     | 
| 
       88 
88 
     | 
    
         | 
| 
       89 
89 
     | 
    
         
             
                  it 'properly detects when a node has changed roles' do
         
     | 
| 
       90 
90 
     | 
    
         
             
                    client.current_master.change_role_to('slave')
         
     | 
| 
       91 
91 
     | 
    
         
             
                    expect { client.send(:master) }.to raise_error(InvalidNodeRoleError)
         
     | 
| 
       92 
92 
     | 
    
         
             
                  end
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
                  it 'raises error for unsupported operations' do
         
     | 
| 
      
 95 
     | 
    
         
            +
                    expect { client.select }.to raise_error(UnsupportedOperationError)
         
     | 
| 
      
 96 
     | 
    
         
            +
                  end
         
     | 
| 
       93 
97 
     | 
    
         
             
                end
         
     | 
| 
       94 
98 
     | 
    
         
             
              end
         
     | 
| 
       95 
99 
     | 
    
         
             
            end
         
     | 
    
        data/spec/node_manager_spec.rb
    CHANGED
    
    | 
         @@ -9,17 +9,17 @@ module RedisFailover 
     | 
|
| 
       9 
9 
     | 
    
         
             
                    manager.nodes.should == {
         
     | 
| 
       10 
10 
     | 
    
         
             
                      :master => 'master:6379',
         
     | 
| 
       11 
11 
     | 
    
         
             
                      :slaves => ['slave:6379'],
         
     | 
| 
       12 
     | 
    
         
            -
                      : 
     | 
| 
      
 12 
     | 
    
         
            +
                      :unavailable => []
         
     | 
| 
       13 
13 
     | 
    
         
             
                    }
         
     | 
| 
       14 
14 
     | 
    
         
             
                  end
         
     | 
| 
       15 
15 
     | 
    
         
             
                end
         
     | 
| 
       16 
16 
     | 
    
         | 
| 
       17 
     | 
    
         
            -
                describe '# 
     | 
| 
      
 17 
     | 
    
         
            +
                describe '#handle_unavailable' do
         
     | 
| 
       18 
18 
     | 
    
         
             
                  context 'slave dies' do
         
     | 
| 
       19 
     | 
    
         
            -
                    it 'moves slave to  
     | 
| 
      
 19 
     | 
    
         
            +
                    it 'moves slave to unavailable list' do
         
     | 
| 
       20 
20 
     | 
    
         
             
                      slave = manager.slaves.first
         
     | 
| 
       21 
     | 
    
         
            -
                      manager. 
     | 
| 
       22 
     | 
    
         
            -
                      manager.nodes[: 
     | 
| 
      
 21 
     | 
    
         
            +
                      manager.force_unavailable(slave)
         
     | 
| 
      
 22 
     | 
    
         
            +
                      manager.nodes[:unavailable].should include(slave.to_s)
         
     | 
| 
       23 
23 
     | 
    
         
             
                    end
         
     | 
| 
       24 
24 
     | 
    
         
             
                  end
         
     | 
| 
       25 
25 
     | 
    
         | 
| 
         @@ -27,43 +27,57 @@ module RedisFailover 
     | 
|
| 
       27 
27 
     | 
    
         
             
                    before(:each) do
         
     | 
| 
       28 
28 
     | 
    
         
             
                      @slave = manager.slaves.first
         
     | 
| 
       29 
29 
     | 
    
         
             
                      @master = manager.master
         
     | 
| 
       30 
     | 
    
         
            -
                      manager. 
     | 
| 
      
 30 
     | 
    
         
            +
                      manager.force_unavailable(@master)
         
     | 
| 
       31 
31 
     | 
    
         
             
                    end
         
     | 
| 
       32 
32 
     | 
    
         | 
| 
       33 
33 
     | 
    
         
             
                    it 'promotes slave to master' do
         
     | 
| 
       34 
34 
     | 
    
         
             
                      manager.master.should == @slave
         
     | 
| 
       35 
35 
     | 
    
         
             
                    end
         
     | 
| 
       36 
36 
     | 
    
         | 
| 
       37 
     | 
    
         
            -
                    it 'moves master to  
     | 
| 
       38 
     | 
    
         
            -
                      manager.nodes[: 
     | 
| 
      
 37 
     | 
    
         
            +
                    it 'moves master to unavailable list' do
         
     | 
| 
      
 38 
     | 
    
         
            +
                      manager.nodes[:unavailable].should include(@master.to_s)
         
     | 
| 
       39 
39 
     | 
    
         
             
                    end
         
     | 
| 
       40 
40 
     | 
    
         
             
                  end
         
     | 
| 
       41 
41 
     | 
    
         
             
                end
         
     | 
| 
       42 
42 
     | 
    
         | 
| 
       43 
     | 
    
         
            -
                describe '# 
     | 
| 
      
 43 
     | 
    
         
            +
                describe '#handle_available' do
         
     | 
| 
       44 
44 
     | 
    
         
             
                  before(:each) do
         
     | 
| 
       45 
     | 
    
         
            -
                    # force to be  
     | 
| 
      
 45 
     | 
    
         
            +
                    # force to be unavailable first
         
     | 
| 
       46 
46 
     | 
    
         
             
                    @slave = manager.slaves.first
         
     | 
| 
       47 
     | 
    
         
            -
                    manager. 
     | 
| 
      
 47 
     | 
    
         
            +
                    manager.force_unavailable(@slave)
         
     | 
| 
       48 
48 
     | 
    
         
             
                  end
         
     | 
| 
       49 
49 
     | 
    
         | 
| 
       50 
50 
     | 
    
         
             
                  context 'slave node with a master present' do
         
     | 
| 
       51 
     | 
    
         
            -
                    it 'removes slave from  
     | 
| 
       52 
     | 
    
         
            -
                      manager. 
     | 
| 
       53 
     | 
    
         
            -
                      manager.nodes[: 
     | 
| 
      
 51 
     | 
    
         
            +
                    it 'removes slave from unavailable list' do
         
     | 
| 
      
 52 
     | 
    
         
            +
                      manager.force_available(@slave)
         
     | 
| 
      
 53 
     | 
    
         
            +
                      manager.nodes[:unavailable].should be_empty
         
     | 
| 
       54 
54 
     | 
    
         
             
                      manager.nodes[:slaves].should include(@slave.to_s)
         
     | 
| 
       55 
55 
     | 
    
         
             
                    end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                    it 'makes node a slave of new master' do
         
     | 
| 
      
 58 
     | 
    
         
            +
                      manager.master = Node.new(:host => 'foo', :port => '7892')
         
     | 
| 
      
 59 
     | 
    
         
            +
                      manager.force_available(@slave)
         
     | 
| 
      
 60 
     | 
    
         
            +
                      @slave.fetch_info.should == {
         
     | 
| 
      
 61 
     | 
    
         
            +
                        :role => 'slave',
         
     | 
| 
      
 62 
     | 
    
         
            +
                        :master_host => 'foo',
         
     | 
| 
      
 63 
     | 
    
         
            +
                        :master_port => '7892'}
         
     | 
| 
      
 64 
     | 
    
         
            +
                    end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                    it 'does not invoke slaveof operation if master has not changed' do
         
     | 
| 
      
 67 
     | 
    
         
            +
                      @slave.redis.should_not_receive(:slaveof)
         
     | 
| 
      
 68 
     | 
    
         
            +
                      manager.force_available(@slave)
         
     | 
| 
      
 69 
     | 
    
         
            +
                    end
         
     | 
| 
       56 
70 
     | 
    
         
             
                  end
         
     | 
| 
       57 
71 
     | 
    
         | 
| 
       58 
72 
     | 
    
         
             
                  context 'slave node with no master present' do
         
     | 
| 
       59 
73 
     | 
    
         
             
                    before(:each) do
         
     | 
| 
       60 
74 
     | 
    
         
             
                      @master = manager.master
         
     | 
| 
       61 
     | 
    
         
            -
                      manager. 
     | 
| 
      
 75 
     | 
    
         
            +
                      manager.force_unavailable(@master)
         
     | 
| 
       62 
76 
     | 
    
         
             
                    end
         
     | 
| 
       63 
77 
     | 
    
         | 
| 
       64 
78 
     | 
    
         
             
                    it 'promotes slave to master' do
         
     | 
| 
       65 
79 
     | 
    
         
             
                      manager.master.should be_nil
         
     | 
| 
       66 
     | 
    
         
            -
                      manager. 
     | 
| 
      
 80 
     | 
    
         
            +
                      manager.force_available(@slave)
         
     | 
| 
       67 
81 
     | 
    
         
             
                      manager.master.should == @slave
         
     | 
| 
       68 
82 
     | 
    
         
             
                    end
         
     | 
| 
       69 
83 
     | 
    
         | 
| 
         @@ -72,5 +86,24 @@ module RedisFailover 
     | 
|
| 
       72 
86 
     | 
    
         
             
                    end
         
     | 
| 
       73 
87 
     | 
    
         
             
                  end
         
     | 
| 
       74 
88 
     | 
    
         
             
                end
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                describe '#handle_syncing' do
         
     | 
| 
      
 91 
     | 
    
         
            +
                  context 'prohibits stale reads' do
         
     | 
| 
      
 92 
     | 
    
         
            +
                    it 'adds node to unavailable list' do
         
     | 
| 
      
 93 
     | 
    
         
            +
                      slave = manager.slaves.first
         
     | 
| 
      
 94 
     | 
    
         
            +
                      manager.force_syncing(slave, false)
         
     | 
| 
      
 95 
     | 
    
         
            +
                      manager.nodes[:unavailable].should include(slave.to_s)
         
     | 
| 
      
 96 
     | 
    
         
            +
                    end
         
     | 
| 
      
 97 
     | 
    
         
            +
                  end
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                  context 'allows stale reads' do
         
     | 
| 
      
 100 
     | 
    
         
            +
                    it 'makes node available' do
         
     | 
| 
      
 101 
     | 
    
         
            +
                      slave = manager.slaves.first
         
     | 
| 
      
 102 
     | 
    
         
            +
                      manager.force_syncing(slave, true)
         
     | 
| 
      
 103 
     | 
    
         
            +
                      manager.nodes[:unavailable].should_not include(slave.to_s)
         
     | 
| 
      
 104 
     | 
    
         
            +
                      manager.nodes[:slaves].should include(slave.to_s)
         
     | 
| 
      
 105 
     | 
    
         
            +
                    end
         
     | 
| 
      
 106 
     | 
    
         
            +
                  end
         
     | 
| 
      
 107 
     | 
    
         
            +
                end
         
     | 
| 
       75 
108 
     | 
    
         
             
              end
         
     | 
| 
       76 
109 
     | 
    
         
             
            end
         
     | 
    
        data/spec/node_spec.rb
    CHANGED
    
    | 
         @@ -20,13 +20,13 @@ module RedisFailover 
     | 
|
| 
       20 
20 
     | 
    
         
             
                end
         
     | 
| 
       21 
21 
     | 
    
         | 
| 
       22 
22 
     | 
    
         
             
                describe '#ping' do
         
     | 
| 
       23 
     | 
    
         
            -
                  it 'responds properly if node is  
     | 
| 
      
 23 
     | 
    
         
            +
                  it 'responds properly if node is available' do
         
     | 
| 
       24 
24 
     | 
    
         
             
                    expect { node.ping }.to_not raise_error
         
     | 
| 
       25 
25 
     | 
    
         
             
                  end
         
     | 
| 
       26 
26 
     | 
    
         | 
| 
       27 
     | 
    
         
            -
                  it 'responds properly if node is  
     | 
| 
       28 
     | 
    
         
            -
                    node.redis. 
     | 
| 
       29 
     | 
    
         
            -
                    expect { node.ping }.to raise_error( 
     | 
| 
      
 27 
     | 
    
         
            +
                  it 'responds properly if node is unavailable' do
         
     | 
| 
      
 28 
     | 
    
         
            +
                    node.redis.make_unavailable!
         
     | 
| 
      
 29 
     | 
    
         
            +
                    expect { node.ping }.to raise_error(NodeUnavailableError)
         
     | 
| 
       30 
30 
     | 
    
         
             
                  end
         
     | 
| 
       31 
31 
     | 
    
         
             
                end
         
     | 
| 
       32 
32 
     | 
    
         | 
| 
         @@ -53,24 +53,32 @@ module RedisFailover 
     | 
|
| 
       53 
53 
     | 
    
         
             
                  end
         
     | 
| 
       54 
54 
     | 
    
         
             
                end
         
     | 
| 
       55 
55 
     | 
    
         | 
| 
       56 
     | 
    
         
            -
                describe '# 
     | 
| 
      
 56 
     | 
    
         
            +
                describe '#wait' do
         
     | 
| 
       57 
57 
     | 
    
         
             
                  it 'should wait until node dies' do
         
     | 
| 
       58 
     | 
    
         
            -
                    thread = Thread.new { node. 
     | 
| 
      
 58 
     | 
    
         
            +
                    thread = Thread.new { node.wait }
         
     | 
| 
       59 
59 
     | 
    
         
             
                    thread.should be_alive
         
     | 
| 
       60 
     | 
    
         
            -
                    node.redis. 
     | 
| 
      
 60 
     | 
    
         
            +
                    node.redis.make_unavailable!
         
     | 
| 
       61 
61 
     | 
    
         
             
                    expect { thread.value }.to raise_error
         
     | 
| 
       62 
62 
     | 
    
         
             
                  end
         
     | 
| 
       63 
63 
     | 
    
         
             
                end
         
     | 
| 
       64 
64 
     | 
    
         | 
| 
       65 
     | 
    
         
            -
                describe '# 
     | 
| 
      
 65 
     | 
    
         
            +
                describe '#wakeup' do
         
     | 
| 
       66 
66 
     | 
    
         
             
                  it 'should gracefully stop waiting' do
         
     | 
| 
       67 
     | 
    
         
            -
                    thread = Thread.new { node. 
     | 
| 
      
 67 
     | 
    
         
            +
                    thread = Thread.new { node.wait }
         
     | 
| 
       68 
68 
     | 
    
         
             
                    thread.should be_alive
         
     | 
| 
       69 
     | 
    
         
            -
                    node. 
     | 
| 
      
 69 
     | 
    
         
            +
                    node.wakeup
         
     | 
| 
       70 
70 
     | 
    
         
             
                    sleep 0.2
         
     | 
| 
       71 
71 
     | 
    
         
             
                    thread.should_not be_alive
         
     | 
| 
       72 
72 
     | 
    
         
             
                    thread.value.should be_nil
         
     | 
| 
       73 
73 
     | 
    
         
             
                  end
         
     | 
| 
       74 
74 
     | 
    
         
             
                end
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                describe '#perform_operation' do
         
     | 
| 
      
 77 
     | 
    
         
            +
                  it 'raises error for any operation that hangs for too long' do
         
     | 
| 
      
 78 
     | 
    
         
            +
                    expect do
         
     | 
| 
      
 79 
     | 
    
         
            +
                      node.send(:perform_operation) { 1_000_000.times { sleep 0.1 } }
         
     | 
| 
      
 80 
     | 
    
         
            +
                    end.to raise_error(NodeUnavailableError)
         
     | 
| 
      
 81 
     | 
    
         
            +
                  end
         
     | 
| 
      
 82 
     | 
    
         
            +
                end
         
     | 
| 
       75 
83 
     | 
    
         
             
              end
         
     | 
| 
       76 
84 
     | 
    
         
             
            end
         
     | 
    
        data/spec/node_watcher_spec.rb
    CHANGED
    
    | 
         @@ -20,23 +20,38 @@ module RedisFailover 
     | 
|
| 
       20 
20 
     | 
    
         
             
                let(:node) { Node.new(:host => 'host', :port => 123).extend(RedisStubSupport) }
         
     | 
| 
       21 
21 
     | 
    
         | 
| 
       22 
22 
     | 
    
         
             
                describe '#watch' do
         
     | 
| 
       23 
     | 
    
         
            -
                   
     | 
| 
       24 
     | 
    
         
            -
                     
     | 
| 
       25 
     | 
    
         
            -
             
     | 
| 
       26 
     | 
    
         
            -
             
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
       28 
     | 
    
         
            -
             
     | 
| 
       29 
     | 
    
         
            -
             
     | 
| 
       30 
     | 
    
         
            -
             
     | 
| 
      
 23 
     | 
    
         
            +
                  context 'node is not syncing with master' do
         
     | 
| 
      
 24 
     | 
    
         
            +
                    it 'properly informs manager of unavailable node' do
         
     | 
| 
      
 25 
     | 
    
         
            +
                      watcher = NodeWatcher.new(node_manager, node, 1)
         
     | 
| 
      
 26 
     | 
    
         
            +
                      watcher.watch
         
     | 
| 
      
 27 
     | 
    
         
            +
                      sleep(3)
         
     | 
| 
      
 28 
     | 
    
         
            +
                      node.redis.make_unavailable!
         
     | 
| 
      
 29 
     | 
    
         
            +
                      sleep(3)
         
     | 
| 
      
 30 
     | 
    
         
            +
                      watcher.shutdown
         
     | 
| 
      
 31 
     | 
    
         
            +
                      node_manager.state_for(node).should == :unavailable
         
     | 
| 
      
 32 
     | 
    
         
            +
                    end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                    it 'properly informs manager of available node' do
         
     | 
| 
      
 35 
     | 
    
         
            +
                      node_manager.notify_state_change(node, :unavailable)
         
     | 
| 
      
 36 
     | 
    
         
            +
                      watcher = NodeWatcher.new(node_manager, node, 1)
         
     | 
| 
      
 37 
     | 
    
         
            +
                      watcher.watch
         
     | 
| 
      
 38 
     | 
    
         
            +
                      sleep(3)
         
     | 
| 
      
 39 
     | 
    
         
            +
                      watcher.shutdown
         
     | 
| 
      
 40 
     | 
    
         
            +
                      node_manager.state_for(node).should == :available
         
     | 
| 
      
 41 
     | 
    
         
            +
                    end
         
     | 
| 
       31 
42 
     | 
    
         
             
                  end
         
     | 
| 
       32 
43 
     | 
    
         | 
| 
       33 
     | 
    
         
            -
                   
     | 
| 
       34 
     | 
    
         
            -
                     
     | 
| 
       35 
     | 
    
         
            -
             
     | 
| 
       36 
     | 
    
         
            -
             
     | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
       38 
     | 
    
         
            -
             
     | 
| 
       39 
     | 
    
         
            -
             
     | 
| 
      
 44 
     | 
    
         
            +
                  context 'node is syncing with master' do
         
     | 
| 
      
 45 
     | 
    
         
            +
                    it 'properly informs manager of syncing node' do
         
     | 
| 
      
 46 
     | 
    
         
            +
                      node_manager.notify_state_change(node, :unavailable)
         
     | 
| 
      
 47 
     | 
    
         
            +
                      node.redis.slaveof('masterhost', 9876)
         
     | 
| 
      
 48 
     | 
    
         
            +
                      node.redis.force_sync_with_master(true)
         
     | 
| 
      
 49 
     | 
    
         
            +
                      watcher = NodeWatcher.new(node_manager, node, 1)
         
     | 
| 
      
 50 
     | 
    
         
            +
                      watcher.watch
         
     | 
| 
      
 51 
     | 
    
         
            +
                      sleep(3)
         
     | 
| 
      
 52 
     | 
    
         
            +
                      watcher.shutdown
         
     | 
| 
      
 53 
     | 
    
         
            +
                      node_manager.state_for(node).should == :syncing
         
     | 
| 
      
 54 
     | 
    
         
            +
                    end
         
     | 
| 
       40 
55 
     | 
    
         
             
                  end
         
     | 
| 
       41 
56 
     | 
    
         
             
                end
         
     | 
| 
       42 
57 
     | 
    
         
             
              end
         
     | 
| 
         @@ -1,5 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module RedisFailover
         
     | 
| 
       2 
2 
     | 
    
         
             
              class NodeManagerStub < NodeManager
         
     | 
| 
      
 3 
     | 
    
         
            +
                attr_accessor :master
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
       3 
5 
     | 
    
         
             
                def parse_nodes
         
     | 
| 
       4 
6 
     | 
    
         
             
                  master = Node.new(:host => 'master')
         
     | 
| 
       5 
7 
     | 
    
         
             
                  slave = Node.new(:host => 'slave')
         
     | 
| 
         @@ -9,10 +11,6 @@ module RedisFailover 
     | 
|
| 
       9 
11 
     | 
    
         
             
                  [master, [slave]]
         
     | 
| 
       10 
12 
     | 
    
         
             
                end
         
     | 
| 
       11 
13 
     | 
    
         | 
| 
       12 
     | 
    
         
            -
                def master
         
     | 
| 
       13 
     | 
    
         
            -
                  @master
         
     | 
| 
       14 
     | 
    
         
            -
                end
         
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
       16 
14 
     | 
    
         
             
                def slaves
         
     | 
| 
       17 
15 
     | 
    
         
             
                  @slaves
         
     | 
| 
       18 
16 
     | 
    
         
             
                end
         
     | 
| 
         @@ -26,17 +24,24 @@ module RedisFailover 
     | 
|
| 
       26 
24 
     | 
    
         
             
                  @thread.value
         
     | 
| 
       27 
25 
     | 
    
         
             
                end
         
     | 
| 
       28 
26 
     | 
    
         | 
| 
       29 
     | 
    
         
            -
                def  
     | 
| 
      
 27 
     | 
    
         
            +
                def force_unavailable(node)
         
     | 
| 
      
 28 
     | 
    
         
            +
                  start_processing
         
     | 
| 
      
 29 
     | 
    
         
            +
                  node.redis.make_unavailable!
         
     | 
| 
      
 30 
     | 
    
         
            +
                  notify_state_change(node, :unavailable)
         
     | 
| 
      
 31 
     | 
    
         
            +
                  stop_processing
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                def force_available(node)
         
     | 
| 
       30 
35 
     | 
    
         
             
                  start_processing
         
     | 
| 
       31 
     | 
    
         
            -
                  node.redis. 
     | 
| 
       32 
     | 
    
         
            -
                  notify_state_change(node, : 
     | 
| 
      
 36 
     | 
    
         
            +
                  node.redis.make_available!
         
     | 
| 
      
 37 
     | 
    
         
            +
                  notify_state_change(node, :available)
         
     | 
| 
       33 
38 
     | 
    
         
             
                  stop_processing
         
     | 
| 
       34 
39 
     | 
    
         
             
                end
         
     | 
| 
       35 
40 
     | 
    
         | 
| 
       36 
     | 
    
         
            -
                def  
     | 
| 
      
 41 
     | 
    
         
            +
                def force_syncing(node, serve_stale_reads)
         
     | 
| 
       37 
42 
     | 
    
         
             
                  start_processing
         
     | 
| 
       38 
     | 
    
         
            -
                  node.redis. 
     | 
| 
       39 
     | 
    
         
            -
                  notify_state_change(node, : 
     | 
| 
      
 43 
     | 
    
         
            +
                  node.redis.force_sync_with_master(serve_stale_reads)
         
     | 
| 
      
 44 
     | 
    
         
            +
                  notify_state_change(node, :syncing)
         
     | 
| 
       40 
45 
     | 
    
         
             
                  stop_processing
         
     | 
| 
       41 
46 
     | 
    
         
             
                end
         
     | 
| 
       42 
47 
     | 
    
         
             
              end
         
     | 
    
        data/spec/support/redis_stub.rb
    CHANGED
    
    | 
         @@ -6,6 +6,7 @@ module RedisFailover 
     | 
|
| 
       6 
6 
     | 
    
         
             
                class Proxy
         
     | 
| 
       7 
7 
     | 
    
         
             
                  def initialize(queue, opts = {})
         
     | 
| 
       8 
8 
     | 
    
         
             
                    @info = {'role' => 'master'}
         
     | 
| 
      
 9 
     | 
    
         
            +
                    @config = {'slave-serve-stale-data' => 'yes'}
         
     | 
| 
       9 
10 
     | 
    
         
             
                    @queue = queue
         
     | 
| 
       10 
11 
     | 
    
         
             
                  end
         
     | 
| 
       11 
12 
     | 
    
         | 
| 
         @@ -25,8 +26,12 @@ module RedisFailover 
     | 
|
| 
       25 
26 
     | 
    
         
             
                  def slaveof(host, port)
         
     | 
| 
       26 
27 
     | 
    
         
             
                    if host == 'no' && port == 'one'
         
     | 
| 
       27 
28 
     | 
    
         
             
                      @info['role'] = 'master'
         
     | 
| 
      
 29 
     | 
    
         
            +
                      @info.delete('master_host')
         
     | 
| 
      
 30 
     | 
    
         
            +
                      @info.delete('master_port')
         
     | 
| 
       28 
31 
     | 
    
         
             
                    else
         
     | 
| 
       29 
32 
     | 
    
         
             
                      @info['role'] = 'slave'
         
     | 
| 
      
 33 
     | 
    
         
            +
                      @info['master_host'] = host
         
     | 
| 
      
 34 
     | 
    
         
            +
                      @info['master_port'] = port.to_s
         
     | 
| 
       30 
35 
     | 
    
         
             
                    end
         
     | 
| 
       31 
36 
     | 
    
         
             
                  end
         
     | 
| 
       32 
37 
     | 
    
         | 
| 
         @@ -34,26 +39,35 @@ module RedisFailover 
     | 
|
| 
       34 
39 
     | 
    
         
             
                    @info.dup
         
     | 
| 
       35 
40 
     | 
    
         
             
                  end
         
     | 
| 
       36 
41 
     | 
    
         | 
| 
       37 
     | 
    
         
            -
                  def ping
         
     | 
| 
       38 
     | 
    
         
            -
                    'pong'
         
     | 
| 
       39 
     | 
    
         
            -
                  end
         
     | 
| 
       40 
     | 
    
         
            -
             
     | 
| 
       41 
42 
     | 
    
         
             
                  def change_role_to(role)
         
     | 
| 
       42 
43 
     | 
    
         
             
                    @info['role'] = role
         
     | 
| 
       43 
44 
     | 
    
         
             
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  def config(action, attribute)
         
     | 
| 
      
 47 
     | 
    
         
            +
                    [action, @config[attribute]]
         
     | 
| 
      
 48 
     | 
    
         
            +
                  end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                  def force_sync_with_master(serve_stale_reads)
         
     | 
| 
      
 51 
     | 
    
         
            +
                    @config['slave-serve-stale-data'] = serve_stale_reads ? 'yes' : 'no'
         
     | 
| 
      
 52 
     | 
    
         
            +
                    @info['master_sync_in_progress'] = '1'
         
     | 
| 
      
 53 
     | 
    
         
            +
                  end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                  def force_sync_done
         
     | 
| 
      
 56 
     | 
    
         
            +
                    @info['master_sync_in_progress'] = '0'
         
     | 
| 
      
 57 
     | 
    
         
            +
                  end
         
     | 
| 
       44 
58 
     | 
    
         
             
                end
         
     | 
| 
       45 
59 
     | 
    
         | 
| 
       46 
     | 
    
         
            -
                attr_reader :host, :port, : 
     | 
| 
      
 60 
     | 
    
         
            +
                attr_reader :host, :port, :available
         
     | 
| 
       47 
61 
     | 
    
         
             
                def initialize(opts = {})
         
     | 
| 
       48 
62 
     | 
    
         
             
                  @host = opts[:host]
         
     | 
| 
       49 
63 
     | 
    
         
             
                  @port = Integer(opts[:port])
         
     | 
| 
       50 
64 
     | 
    
         
             
                  @queue = Queue.new
         
     | 
| 
       51 
65 
     | 
    
         
             
                  @proxy = Proxy.new(@queue, opts)
         
     | 
| 
       52 
     | 
    
         
            -
                  @ 
     | 
| 
      
 66 
     | 
    
         
            +
                  @available = true
         
     | 
| 
       53 
67 
     | 
    
         
             
                end
         
     | 
| 
       54 
68 
     | 
    
         | 
| 
       55 
69 
     | 
    
         
             
                def method_missing(method, *args, &block)
         
     | 
| 
       56 
     | 
    
         
            -
                  if @ 
     | 
| 
      
 70 
     | 
    
         
            +
                  if @available
         
     | 
| 
       57 
71 
     | 
    
         
             
                    @proxy.send(method, *args, &block)
         
     | 
| 
       58 
72 
     | 
    
         
             
                  else
         
     | 
| 
       59 
73 
     | 
    
         
             
                    raise Errno::ECONNREFUSED
         
     | 
| 
         @@ -64,13 +78,13 @@ module RedisFailover 
     | 
|
| 
       64 
78 
     | 
    
         
             
                  @proxy.change_role_to(role)
         
     | 
| 
       65 
79 
     | 
    
         
             
                end
         
     | 
| 
       66 
80 
     | 
    
         | 
| 
       67 
     | 
    
         
            -
                def  
     | 
| 
       68 
     | 
    
         
            -
                  @ 
     | 
| 
      
 81 
     | 
    
         
            +
                def make_available!
         
     | 
| 
      
 82 
     | 
    
         
            +
                  @available = true
         
     | 
| 
       69 
83 
     | 
    
         
             
                end
         
     | 
| 
       70 
84 
     | 
    
         | 
| 
       71 
     | 
    
         
            -
                def  
     | 
| 
      
 85 
     | 
    
         
            +
                def make_unavailable!
         
     | 
| 
       72 
86 
     | 
    
         
             
                  @queue << Errno::ECONNREFUSED
         
     | 
| 
       73 
     | 
    
         
            -
                  @ 
     | 
| 
      
 87 
     | 
    
         
            +
                  @available = false
         
     | 
| 
       74 
88 
     | 
    
         
             
                end
         
     | 
| 
       75 
89 
     | 
    
         | 
| 
       76 
90 
     | 
    
         
             
                def to_s
         
     | 
| 
         @@ -86,5 +100,6 @@ module RedisFailover 
     | 
|
| 
       86 
100 
     | 
    
         
             
                def redis
         
     | 
| 
       87 
101 
     | 
    
         
             
                  @redis ||= RedisStub.new(:host => @host, :port => @port)
         
     | 
| 
       88 
102 
     | 
    
         
             
                end
         
     | 
| 
      
 103 
     | 
    
         
            +
                alias_method :new_client, :redis
         
     | 
| 
       89 
104 
     | 
    
         
             
              end
         
     | 
| 
       90 
105 
     | 
    
         
             
            end
         
     | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: redis_failover
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 0. 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 0.4.0
         
     | 
| 
       5 
5 
     | 
    
         
             
              prerelease: 
         
     | 
| 
       6 
6 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       7 
7 
     | 
    
         
             
            authors:
         
     | 
| 
         @@ -9,11 +9,11 @@ authors: 
     | 
|
| 
       9 
9 
     | 
    
         
             
            autorequire: 
         
     | 
| 
       10 
10 
     | 
    
         
             
            bindir: bin
         
     | 
| 
       11 
11 
     | 
    
         
             
            cert_chain: []
         
     | 
| 
       12 
     | 
    
         
            -
            date: 2012-04- 
     | 
| 
      
 12 
     | 
    
         
            +
            date: 2012-04-15 00:00:00.000000000 Z
         
     | 
| 
       13 
13 
     | 
    
         
             
            dependencies:
         
     | 
| 
       14 
14 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       15 
15 
     | 
    
         
             
              name: redis
         
     | 
| 
       16 
     | 
    
         
            -
              requirement: & 
     | 
| 
      
 16 
     | 
    
         
            +
              requirement: &70210559246440 !ruby/object:Gem::Requirement
         
     | 
| 
       17 
17 
     | 
    
         
             
                none: false
         
     | 
| 
       18 
18 
     | 
    
         
             
                requirements:
         
     | 
| 
       19 
19 
     | 
    
         
             
                - - ! '>='
         
     | 
| 
         @@ -21,10 +21,10 @@ dependencies: 
     | 
|
| 
       21 
21 
     | 
    
         
             
                    version: '0'
         
     | 
| 
       22 
22 
     | 
    
         
             
              type: :runtime
         
     | 
| 
       23 
23 
     | 
    
         
             
              prerelease: false
         
     | 
| 
       24 
     | 
    
         
            -
              version_requirements: * 
     | 
| 
      
 24 
     | 
    
         
            +
              version_requirements: *70210559246440
         
     | 
| 
       25 
25 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       26 
26 
     | 
    
         
             
              name: redis-namespace
         
     | 
| 
       27 
     | 
    
         
            -
              requirement: & 
     | 
| 
      
 27 
     | 
    
         
            +
              requirement: &70210559246000 !ruby/object:Gem::Requirement
         
     | 
| 
       28 
28 
     | 
    
         
             
                none: false
         
     | 
| 
       29 
29 
     | 
    
         
             
                requirements:
         
     | 
| 
       30 
30 
     | 
    
         
             
                - - ! '>='
         
     | 
| 
         @@ -32,10 +32,10 @@ dependencies: 
     | 
|
| 
       32 
32 
     | 
    
         
             
                    version: '0'
         
     | 
| 
       33 
33 
     | 
    
         
             
              type: :runtime
         
     | 
| 
       34 
34 
     | 
    
         
             
              prerelease: false
         
     | 
| 
       35 
     | 
    
         
            -
              version_requirements: * 
     | 
| 
      
 35 
     | 
    
         
            +
              version_requirements: *70210559246000
         
     | 
| 
       36 
36 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       37 
37 
     | 
    
         
             
              name: multi_json
         
     | 
| 
       38 
     | 
    
         
            -
              requirement: & 
     | 
| 
      
 38 
     | 
    
         
            +
              requirement: &70210559245580 !ruby/object:Gem::Requirement
         
     | 
| 
       39 
39 
     | 
    
         
             
                none: false
         
     | 
| 
       40 
40 
     | 
    
         
             
                requirements:
         
     | 
| 
       41 
41 
     | 
    
         
             
                - - ! '>='
         
     | 
| 
         @@ -43,10 +43,10 @@ dependencies: 
     | 
|
| 
       43 
43 
     | 
    
         
             
                    version: '0'
         
     | 
| 
       44 
44 
     | 
    
         
             
              type: :runtime
         
     | 
| 
       45 
45 
     | 
    
         
             
              prerelease: false
         
     | 
| 
       46 
     | 
    
         
            -
              version_requirements: * 
     | 
| 
      
 46 
     | 
    
         
            +
              version_requirements: *70210559245580
         
     | 
| 
       47 
47 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       48 
48 
     | 
    
         
             
              name: sinatra
         
     | 
| 
       49 
     | 
    
         
            -
              requirement: & 
     | 
| 
      
 49 
     | 
    
         
            +
              requirement: &70210559245160 !ruby/object:Gem::Requirement
         
     | 
| 
       50 
50 
     | 
    
         
             
                none: false
         
     | 
| 
       51 
51 
     | 
    
         
             
                requirements:
         
     | 
| 
       52 
52 
     | 
    
         
             
                - - ! '>='
         
     | 
| 
         @@ -54,10 +54,10 @@ dependencies: 
     | 
|
| 
       54 
54 
     | 
    
         
             
                    version: '0'
         
     | 
| 
       55 
55 
     | 
    
         
             
              type: :runtime
         
     | 
| 
       56 
56 
     | 
    
         
             
              prerelease: false
         
     | 
| 
       57 
     | 
    
         
            -
              version_requirements: * 
     | 
| 
      
 57 
     | 
    
         
            +
              version_requirements: *70210559245160
         
     | 
| 
       58 
58 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       59 
59 
     | 
    
         
             
              name: rake
         
     | 
| 
       60 
     | 
    
         
            -
              requirement: & 
     | 
| 
      
 60 
     | 
    
         
            +
              requirement: &70210559244740 !ruby/object:Gem::Requirement
         
     | 
| 
       61 
61 
     | 
    
         
             
                none: false
         
     | 
| 
       62 
62 
     | 
    
         
             
                requirements:
         
     | 
| 
       63 
63 
     | 
    
         
             
                - - ! '>='
         
     | 
| 
         @@ -65,10 +65,10 @@ dependencies: 
     | 
|
| 
       65 
65 
     | 
    
         
             
                    version: '0'
         
     | 
| 
       66 
66 
     | 
    
         
             
              type: :development
         
     | 
| 
       67 
67 
     | 
    
         
             
              prerelease: false
         
     | 
| 
       68 
     | 
    
         
            -
              version_requirements: * 
     | 
| 
      
 68 
     | 
    
         
            +
              version_requirements: *70210559244740
         
     | 
| 
       69 
69 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       70 
70 
     | 
    
         
             
              name: rspec
         
     | 
| 
       71 
     | 
    
         
            -
              requirement: & 
     | 
| 
      
 71 
     | 
    
         
            +
              requirement: &70210559244320 !ruby/object:Gem::Requirement
         
     | 
| 
       72 
72 
     | 
    
         
             
                none: false
         
     | 
| 
       73 
73 
     | 
    
         
             
                requirements:
         
     | 
| 
       74 
74 
     | 
    
         
             
                - - ! '>='
         
     | 
| 
         @@ -76,7 +76,7 @@ dependencies: 
     | 
|
| 
       76 
76 
     | 
    
         
             
                    version: '0'
         
     | 
| 
       77 
77 
     | 
    
         
             
              type: :development
         
     | 
| 
       78 
78 
     | 
    
         
             
              prerelease: false
         
     | 
| 
       79 
     | 
    
         
            -
              version_requirements: * 
     | 
| 
      
 79 
     | 
    
         
            +
              version_requirements: *70210559244320
         
     | 
| 
       80 
80 
     | 
    
         
             
            description: Redis Failover provides a full automatic master/slave failover solution
         
     | 
| 
       81 
81 
     | 
    
         
             
              for Ruby
         
     | 
| 
       82 
82 
     | 
    
         
             
            email:
         
     | 
| 
         @@ -129,7 +129,7 @@ required_ruby_version: !ruby/object:Gem::Requirement 
     | 
|
| 
       129 
129 
     | 
    
         
             
                  version: '0'
         
     | 
| 
       130 
130 
     | 
    
         
             
                  segments:
         
     | 
| 
       131 
131 
     | 
    
         
             
                  - 0
         
     | 
| 
       132 
     | 
    
         
            -
                  hash: - 
     | 
| 
      
 132 
     | 
    
         
            +
                  hash: -4081812195470581448
         
     | 
| 
       133 
133 
     | 
    
         
             
            required_rubygems_version: !ruby/object:Gem::Requirement
         
     | 
| 
       134 
134 
     | 
    
         
             
              none: false
         
     | 
| 
       135 
135 
     | 
    
         
             
              requirements:
         
     | 
| 
         @@ -138,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement 
     | 
|
| 
       138 
138 
     | 
    
         
             
                  version: '0'
         
     | 
| 
       139 
139 
     | 
    
         
             
                  segments:
         
     | 
| 
       140 
140 
     | 
    
         
             
                  - 0
         
     | 
| 
       141 
     | 
    
         
            -
                  hash: - 
     | 
| 
      
 141 
     | 
    
         
            +
                  hash: -4081812195470581448
         
     | 
| 
       142 
142 
     | 
    
         
             
            requirements: []
         
     | 
| 
       143 
143 
     | 
    
         
             
            rubyforge_project: 
         
     | 
| 
       144 
144 
     | 
    
         
             
            rubygems_version: 1.8.16
         
     |