r4r 0.1.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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +110 -0
- data/.gitignore +12 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +39 -0
- data/LICENSE.txt +21 -0
- data/README.md +15 -0
- data/Rakefile +38 -0
- data/bin/console +14 -0
- data/bin/ghpages +19 -0
- data/bin/rake +2 -0
- data/bin/setup +9 -0
- data/ext/r4r/ring_bits_ext/extconf.rb +7 -0
- data/ext/r4r/ring_bits_ext/ring_bits_ext.c +173 -0
- data/ext/r4r/system_clock_ext/extconf.rb +8 -0
- data/ext/r4r/system_clock_ext/system_clock_ext.c +31 -0
- data/lib/r4r.rb +17 -0
- data/lib/r4r/clock.rb +40 -0
- data/lib/r4r/retry.rb +136 -0
- data/lib/r4r/retry_budget.rb +150 -0
- data/lib/r4r/retry_policy.rb +55 -0
- data/lib/r4r/ring_bits.rb +59 -0
- data/lib/r4r/token_bucket.rb +129 -0
- data/lib/r4r/version.rb +3 -0
- data/lib/r4r/windowed_adder.rb +106 -0
- data/r4r.gemspec +43 -0
- metadata +170 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
#include <ruby.h>
|
2
|
+
#include <sys/time.h>
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Returns current system time in milliseconds.
|
6
|
+
*/
|
7
|
+
static VALUE
|
8
|
+
system_clock_call(VALUE self) {
|
9
|
+
struct timeval tv;
|
10
|
+
long long millis;
|
11
|
+
|
12
|
+
gettimeofday(&tv, NULL);
|
13
|
+
|
14
|
+
millis = ((long long)tv.tv_sec) * 1000;
|
15
|
+
millis = millis + (tv.tv_usec / 1000);
|
16
|
+
|
17
|
+
return LONG2NUM(millis);
|
18
|
+
}
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Module entry point.
|
22
|
+
*/
|
23
|
+
void
|
24
|
+
Init_system_clock_ext() {
|
25
|
+
VALUE mR4r;
|
26
|
+
VALUE cSystemClock;
|
27
|
+
|
28
|
+
mR4r = rb_define_module("R4r");
|
29
|
+
cSystemClock = rb_define_class_under(mR4r, "SystemClockExt", rb_cObject);
|
30
|
+
rb_define_method(cSystemClock, "call", system_clock_call, 0);
|
31
|
+
}
|
data/lib/r4r.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "r4r/version"
|
2
|
+
|
3
|
+
require "r4r/system_clock_ext"
|
4
|
+
require "r4r/clock"
|
5
|
+
|
6
|
+
require "r4r/ring_bits_ext"
|
7
|
+
require "r4r/ring_bits"
|
8
|
+
|
9
|
+
require "r4r/windowed_adder"
|
10
|
+
require "r4r/token_bucket"
|
11
|
+
|
12
|
+
require "r4r/retry_budget"
|
13
|
+
require "r4r/retry_policy"
|
14
|
+
require "r4r/retry"
|
15
|
+
|
16
|
+
module R4r
|
17
|
+
end
|
data/lib/r4r/clock.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module R4r
|
2
|
+
# A system clock
|
3
|
+
#
|
4
|
+
# @abstract
|
5
|
+
class Clock
|
6
|
+
# Returns current system time in milliseconds
|
7
|
+
def call
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# A frozen clock for testing
|
13
|
+
class FrozenClock < Clock
|
14
|
+
# Creates a new instance of frozen clock.
|
15
|
+
#
|
16
|
+
# @param [R4r::Clock] parent an initial time clock
|
17
|
+
def initialize(parent: nil)
|
18
|
+
@time = (parent || R4r.clock).call
|
19
|
+
end
|
20
|
+
|
21
|
+
# @see R4r::Clock#call
|
22
|
+
def call
|
23
|
+
@time
|
24
|
+
end
|
25
|
+
|
26
|
+
# Increase clock time by given seconds.
|
27
|
+
#
|
28
|
+
# @param [Fixnum] seconds a number of seconds to increase time
|
29
|
+
def advance(seconds:)
|
30
|
+
@time += (seconds.to_i * 1_000)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
@@clock = R4r::SystemClockExt.new
|
35
|
+
|
36
|
+
# Default {R4r::Clock} instance.
|
37
|
+
def self.clock
|
38
|
+
@@clock
|
39
|
+
end
|
40
|
+
end
|
data/lib/r4r/retry.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
module R4r
|
2
|
+
|
3
|
+
# An error raises when retry was failed.
|
4
|
+
#
|
5
|
+
class NonRetriableError < RuntimeError
|
6
|
+
attr_reader :cause
|
7
|
+
|
8
|
+
# @param [String] message an error message
|
9
|
+
# @param [Exception] cause a error cause
|
10
|
+
def initialize(message:, cause:)
|
11
|
+
super(message)
|
12
|
+
@cause = cause
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Decorator that wrap blocks and call it within retries.
|
17
|
+
#
|
18
|
+
# @attr [Array[Float]] backoff
|
19
|
+
# @attr [R4r::RetryBudget] budget
|
20
|
+
#
|
21
|
+
# @example constant backoff, it will never pause between retries and will try 3 times.
|
22
|
+
# retry = R4r::Retry.constant_backoff(num_retries: 3)
|
23
|
+
# retry.call { get_http_request }
|
24
|
+
#
|
25
|
+
# @example exponential backoff, it will pause between invocations using given backoff invtervals and will try 4 times
|
26
|
+
# retry = R4r::Retry.backoff(backoff: [0.1, 0.3, 1, 5])
|
27
|
+
# retry.call { get_http_request }
|
28
|
+
#
|
29
|
+
class Retry
|
30
|
+
|
31
|
+
attr_reader :backoff
|
32
|
+
attr_reader :budget
|
33
|
+
|
34
|
+
# Creates a new retries dectorator.
|
35
|
+
#
|
36
|
+
# @param [Array[Float]] backoff an array with backoff intervals (in seconds)
|
37
|
+
# @param [R4r::RetryPolicy] policy a policy used for error filtiring
|
38
|
+
# @param [R4r::RetryBudget] budget a retry budget
|
39
|
+
#
|
40
|
+
# @raise [ArgumentError] when backoff is empty
|
41
|
+
# @raise [ArgumentError] when backoff has negative values
|
42
|
+
def initialize(backoff:, policy: nil, budget: nil)
|
43
|
+
@policy = (policy || R4r::RetryPolicy.always)
|
44
|
+
@backoff = Array.new(backoff).map { |i| i.to_f }
|
45
|
+
@budget = budget != nil ? budget : R4r::RetryBudget.create
|
46
|
+
|
47
|
+
raise ArgumentError, "backoff cannot be empty" if @backoff.empty?
|
48
|
+
raise ArgumentError, "backoff values cannot be negative" unless @backoff.all? {|i| i.to_f >= 0.0 }
|
49
|
+
end
|
50
|
+
|
51
|
+
# Decorates a given block within retries.
|
52
|
+
#
|
53
|
+
# @return [Proc]
|
54
|
+
def decorate(&block)
|
55
|
+
->() { call { yield } }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Calls given block within retries.
|
59
|
+
#
|
60
|
+
# @raise [NonRetriableError]
|
61
|
+
def call(&block)
|
62
|
+
return unless block_given?
|
63
|
+
|
64
|
+
num_retry = 0
|
65
|
+
@budget.deposit
|
66
|
+
|
67
|
+
while num_retry < @backoff.size
|
68
|
+
|
69
|
+
begin
|
70
|
+
return yield(num_retry)
|
71
|
+
rescue => err
|
72
|
+
raise err if err.is_a?(NonRetriableError)
|
73
|
+
|
74
|
+
if (num_retry + 1 == @backoff.size)
|
75
|
+
raise NonRetriableError.new(message: "Retry limit [#{@backoff.size}] reached: #{err}", cause: err)
|
76
|
+
end
|
77
|
+
|
78
|
+
unless @policy.call(error: err, num_retry: num_retry)
|
79
|
+
raise NonRetriableError.new(message: "An error was rejected by policy: #{err}", cause: err)
|
80
|
+
end
|
81
|
+
|
82
|
+
unless @budget.try_withdraw
|
83
|
+
raise NonRetriableError.new(message: "Budget was exhausted: #{err}", cause: err)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
sleep @backoff[num_retry]
|
88
|
+
num_retry += 1
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Creates a {R4r::Retry} with fixed backoff rates.
|
93
|
+
#
|
94
|
+
# @param [R4r::RetryPolicy] policy a policy used for error filtiring
|
95
|
+
# @param [R4r::RetryBudget] budget a retry budget
|
96
|
+
# @return [R4r::Retry]
|
97
|
+
#
|
98
|
+
# @raise [ArgumentError] when num_retries is negative
|
99
|
+
# @raise [ArgumentError] when backoff is negative
|
100
|
+
#
|
101
|
+
# @example without sleep between invocations
|
102
|
+
# R4r::Retry.constant_backoff(num_retries:3)
|
103
|
+
#
|
104
|
+
# @example with sleep 1s between invocations
|
105
|
+
# R4r::Retry.constant_backoff(num_retries: 3, backoff: 1)
|
106
|
+
def self.constant_backoff(num_retries:, backoff: 0.0, policy: nil, budget: nil)
|
107
|
+
raise ArgumentError, "num_retries cannot be negative" unless num_retries.to_i >= 0
|
108
|
+
raise ArgumentError, "backoff cannot be negative" unless backoff.to_f >= 0.0
|
109
|
+
|
110
|
+
backoff = Array.new(num_retries.to_i) { backoff.to_f }
|
111
|
+
R4r::Retry.new(backoff: backoff, policy: policy, budget: budget)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Creates a {R4r::Retry} with backoff intervals.
|
115
|
+
#
|
116
|
+
# @param [Array[Float]] backoff a list of sleep intervals (in seconds)
|
117
|
+
# @param [R4r::RetryPolicy] policy a policy used for error filtiring
|
118
|
+
# @param [R4r::RetryBudget] budget a retry budget
|
119
|
+
# @return [R4r::Retry]
|
120
|
+
#
|
121
|
+
# @raise [ArgumentError] when backoff is nil
|
122
|
+
# @raise [ArgumentError] when backoff isn't array
|
123
|
+
# @raise [ArgumentError] when backoff has negative values
|
124
|
+
#
|
125
|
+
# @example exponential backoff between invocations
|
126
|
+
# R4r::Retry.backoff(backoff: [0.1, 0.5, 1, 3])
|
127
|
+
def self.backoff(backoff:, policy: nil, budget: nil)
|
128
|
+
raise ArgumentError, "backoff cannot be nil" if backoff.nil?
|
129
|
+
raise ArgumentError, "backoff must be an array" unless backoff.is_a?(Array)
|
130
|
+
raise ArgumentError, "backoff values cannot be negative" unless backoff.all? {|i| i.to_f >= 0.0 }
|
131
|
+
|
132
|
+
R4r::Retry.new(backoff: backoff, policy: policy, budget: budget)
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module R4r
|
2
|
+
# Represents a budget for retrying requests.
|
3
|
+
#
|
4
|
+
# A retry budget is useful for attenuating the amplifying effects
|
5
|
+
# of many clients within a process retrying requests multiple
|
6
|
+
# times. This acts as a form of coordination between those retries.
|
7
|
+
#
|
8
|
+
# A Ruby port of the finagle's RetryBudget.
|
9
|
+
#
|
10
|
+
# @abstract
|
11
|
+
# @see https://github.com/twitter/finagle/blob/master/finagle-core/src/main/scala/com/twitter/finagle/service/RetryBudget.scala
|
12
|
+
class RetryBudget
|
13
|
+
|
14
|
+
# Indicates a deposit, or credit, which will typically
|
15
|
+
# permit future withdrawals.
|
16
|
+
def deposit
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
# Check whether or not there is enough balance remaining
|
21
|
+
# to issue a retry, or make a withdrawal.
|
22
|
+
#
|
23
|
+
# @return [Boolean]
|
24
|
+
# `true`, if the retry is allowed and a withdrawal will take place.
|
25
|
+
# `false`, the balance should remain untouched.
|
26
|
+
def try_withdraw
|
27
|
+
raise NotImplementedError
|
28
|
+
end
|
29
|
+
|
30
|
+
# The balance or number of retries that can be made now.
|
31
|
+
def balance
|
32
|
+
raise NotImplementedError
|
33
|
+
end
|
34
|
+
|
35
|
+
# Creates a {R4r::RetryBudget} that allows for about `percent_can_retry` percent
|
36
|
+
# of the total {R4r::RetryBudget#deposit} requests to be retried.
|
37
|
+
#
|
38
|
+
# @param [Fixnum] ttl_ms deposits created by {R4r::RetryBudget#deposit} expire after
|
39
|
+
# approximately `ttl_ms` time has passed. Must be `>= 1 second`
|
40
|
+
# and `<= 60 seconds`.
|
41
|
+
# @param [Fixnum] min_retries_per_second the minimum rate of retries allowed in order to
|
42
|
+
# accommodate clients that have just started issuing requests as well as clients
|
43
|
+
# that do not issue many requests per window.
|
44
|
+
# Must be non-negative and if `0`, then no reserve is given.
|
45
|
+
# @param [Float] percent_can_retry the percentage of calls to `deposit()` that can be
|
46
|
+
# retried. This is in addition to any retries allowed for via `min_retries_per_second`.
|
47
|
+
# Must be >= 0 and <= 1000. As an example, if `0.1` is used, then for every
|
48
|
+
# 10 calls to `deposit()`, 1 retry will be allowed. If `2.0` is used then every
|
49
|
+
# `deposit` allows for 2 retries.
|
50
|
+
# @param [R4r::Clock] clock the current time for testing
|
51
|
+
#
|
52
|
+
# @raise [ArgumentError]
|
53
|
+
def self.create(ttl_ms: nil, min_retries_per_second: nil, percent_can_retry: nil, clock: nil)
|
54
|
+
ttl_ms = (ttl_ms || R4r::TokenRetryBudget::DEFAULT_TTL_MS).to_i
|
55
|
+
min_retries_per_second = (min_retries_per_second || 10).to_i
|
56
|
+
percent_can_retry = (percent_can_retry.to_f || 0.2).to_f
|
57
|
+
|
58
|
+
unless ttl_ms >= 0 && ttl_ms <= 60 * 1000
|
59
|
+
raise ArgumentError, "ttl_ms must be in [1.second, 60.seconds], got #{ttl_ms}"
|
60
|
+
end
|
61
|
+
unless min_retries_per_second >= 0
|
62
|
+
raise ArgumentError, "min_retries_per_second cannot be nagative, got #{min_retries_per_second}"
|
63
|
+
end
|
64
|
+
unless percent_can_retry >= 0.0
|
65
|
+
raise ArgumentError, "percent_can_retry cannot be negative, got #{percent_can_retry}"
|
66
|
+
end
|
67
|
+
unless percent_can_retry <= R4r::TokenRetryBudget::SCALE_FACTOR
|
68
|
+
raise ArgumentError, "percent_can_retry cannot be greater then #{R4r::TokenRetryBudget::SCALE_FACTOR}, got #{percent_can_retry}"
|
69
|
+
end
|
70
|
+
|
71
|
+
if min_retries_per_second == 0 && percent_can_retry == 0.0
|
72
|
+
return R4r::RetryBudget.empty
|
73
|
+
end
|
74
|
+
|
75
|
+
deposit_amount = percent_can_retry == 0.0 ? 0 : R4r::TokenRetryBudget::SCALE_FACTOR.to_i
|
76
|
+
withdrawal_amount = percent_can_retry == 0.0 ? 1 : (R4r::TokenRetryBudget::SCALE_FACTOR / percent_can_retry).to_i
|
77
|
+
reserve = min_retries_per_second * (ttl_ms / 1000) * withdrawal_amount
|
78
|
+
bucket = R4r::LeakyTokenBucket.new(ttl_ms: ttl_ms, reserve: reserve, clock: clock)
|
79
|
+
R4r::TokenRetryBudget.new(bucket: bucket, deposit_amount: deposit_amount, withdrawal_amount: withdrawal_amount)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Creates an empty retry budget.
|
83
|
+
def self.empty
|
84
|
+
EmptyRetryBudget.new
|
85
|
+
end
|
86
|
+
|
87
|
+
# Creates an infinite retry budget.
|
88
|
+
def self.infinite
|
89
|
+
InfiniteRetryBudget.new
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# A {R4r::RetryBudget} that never has a balance,
|
94
|
+
# and as such, will never allow a retry.
|
95
|
+
class EmptyRetryBudget < RetryBudget
|
96
|
+
def deposit ; end
|
97
|
+
def try_withdraw ; false ; end
|
98
|
+
def balance ; 0 ; end
|
99
|
+
end
|
100
|
+
|
101
|
+
# A {R4r::RetryBudget} that always has a balance of `100`,
|
102
|
+
# and as such, will always allow a retry.
|
103
|
+
class InfiniteRetryBudget < RetryBudget
|
104
|
+
def deposit ; end
|
105
|
+
def try_withdraw ; true end
|
106
|
+
def balance ; 100 end
|
107
|
+
end
|
108
|
+
|
109
|
+
class TokenRetryBudget < RetryBudget
|
110
|
+
# This scaling factor allows for `percent_can_retry` > 1 without
|
111
|
+
# having to use floating points (as the underlying mechanism
|
112
|
+
# here is a {R4r::TokenBucket} which is not floating point based).
|
113
|
+
SCALE_FACTOR = 1000.0
|
114
|
+
DEFAULT_TTL_MS = 60 * 1000
|
115
|
+
|
116
|
+
# Creates a new {R4r::TokenRetryBudget}.
|
117
|
+
#
|
118
|
+
# @param [R4r::TokenBucket] bucket
|
119
|
+
# @param [Fixnum] deposit_amount
|
120
|
+
# @param [Fixnum] withdrawal_amount
|
121
|
+
def initialize(bucket:, deposit_amount:, withdrawal_amount:)
|
122
|
+
raise ArgumentError, "bucket cannot be nil" if bucket.nil?
|
123
|
+
raise ArgumentError, "deposit_amount cannot be nil" if deposit_amount.nil?
|
124
|
+
raise ArgumentError, "withdrawal_amount cannot be nil" if withdrawal_amount.nil?
|
125
|
+
|
126
|
+
@bucket = bucket
|
127
|
+
@deposit_amount = deposit_amount.to_i
|
128
|
+
@withdrawal_amount = withdrawal_amount.to_i
|
129
|
+
end
|
130
|
+
|
131
|
+
# @see R4r::RetryBudget#deposit
|
132
|
+
def deposit
|
133
|
+
@bucket.put(@deposit_amount)
|
134
|
+
end
|
135
|
+
|
136
|
+
# @see R4r::RetryBudget#try_withdraw
|
137
|
+
def try_withdraw
|
138
|
+
@bucket.try_get(@withdrawal_amount)
|
139
|
+
end
|
140
|
+
|
141
|
+
# @see R4r::RetryBudget#balance
|
142
|
+
def balance
|
143
|
+
@bucket.count / @withdrawal_amount
|
144
|
+
end
|
145
|
+
|
146
|
+
def to_s
|
147
|
+
"R4r::TokenRetryBudget{deposit=#{@deposit_amount}, withdrawal=#{@withdrawal_amount}, balance=#{balance}}"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module R4r
|
2
|
+
|
3
|
+
# Pluggable retry strategy.
|
4
|
+
#
|
5
|
+
# @abstract
|
6
|
+
class RetryPolicy
|
7
|
+
# Check that given error can be retried or not.
|
8
|
+
#
|
9
|
+
# @param [Exception] error an error was occured
|
10
|
+
# @param [Fixnum] num_retry a number of current retry, started from zero
|
11
|
+
#
|
12
|
+
# @return [Boolean] true if retry can be recovered
|
13
|
+
def call(error:, num_retry:)
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
# Creates a policy that always recover from any kind of errors.
|
18
|
+
#
|
19
|
+
# @return [R4r::RetryPolicy]
|
20
|
+
def self.always
|
21
|
+
->(error:, num_retry:) { true }
|
22
|
+
end
|
23
|
+
|
24
|
+
# Creates a policy that never recover from any kind of errors.
|
25
|
+
#
|
26
|
+
# @return [R4r::RetryPolicy]
|
27
|
+
def self.never
|
28
|
+
->(error:, num_retry:) { false }
|
29
|
+
end
|
30
|
+
|
31
|
+
# Creates a policy that recover from specified kind of errors
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# R4r::RetryPolicy.instance_of(Some::Error, Service::Error)
|
35
|
+
#
|
36
|
+
# @return [R4r::RetryPolicy]
|
37
|
+
def self.instance_of(*klass)
|
38
|
+
R4r::InstanceOfRetryPolicy.new(klass: klass)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# A retry policy that catch specified kind of errors
|
43
|
+
class InstanceOfRetryPolicy < RetryPolicy
|
44
|
+
# @param [Array[Class]] klass an error classes list that used for filtering
|
45
|
+
def initialize(klass:)
|
46
|
+
@klass = klass
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Boolean]
|
50
|
+
# @see R4r::RetryPolicy#call
|
51
|
+
def call(error:, num_retry:)
|
52
|
+
@klass.any? { |kind| error.is_a?(kind) }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module R4r ; class RingBits
|
2
|
+
attr_reader :index, :length, :cardinality
|
3
|
+
|
4
|
+
# Creates a ring bit set whose size is large enough to explicitly
|
5
|
+
# represent bits with indices in the range 0 through
|
6
|
+
# size-1. All bits are initially set to false.
|
7
|
+
#
|
8
|
+
# @param [Fixnum] size the size of ring bits buffer
|
9
|
+
# @raise [ArgumentError] if the specified size is negitive
|
10
|
+
def initialize(size:, bit_set_class: nil)
|
11
|
+
@size = size
|
12
|
+
@bit_set = (bit_set_class || RingBitsExt).new(size.to_i)
|
13
|
+
@is_full = false
|
14
|
+
@index = -1
|
15
|
+
@length = 0
|
16
|
+
@cardinality = 0
|
17
|
+
end
|
18
|
+
|
19
|
+
# Current ring bits buffer size.
|
20
|
+
def size
|
21
|
+
@size
|
22
|
+
end
|
23
|
+
|
24
|
+
# An actual ring bits buffer capacity.
|
25
|
+
def bit_set_size
|
26
|
+
@bit_set.size
|
27
|
+
end
|
28
|
+
|
29
|
+
# Sets the bit at the next index to the specified value.
|
30
|
+
#
|
31
|
+
# @param [Boolean] value is a boolean value to set
|
32
|
+
# @return [Fixnum] the number of bits set to true
|
33
|
+
def set_next(value)
|
34
|
+
increase_length
|
35
|
+
|
36
|
+
new_index = (@index + 1) % @size
|
37
|
+
previous = @bit_set.set(new_index, value == true) ? 1 : 0
|
38
|
+
current = value == true ? 1 : 0
|
39
|
+
|
40
|
+
|
41
|
+
@index = new_index
|
42
|
+
@cardinality = @cardinality - previous + current
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def increase_length
|
48
|
+
return if @is_full
|
49
|
+
|
50
|
+
next_length = @length + 1
|
51
|
+
if (next_length < @size)
|
52
|
+
@length = next_length
|
53
|
+
else
|
54
|
+
@length = size
|
55
|
+
@is_full = true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end ; end
|