riak-client 1.0.5 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.document +5 -0
  2. data/.gitignore +4 -3
  3. data/.rspec +1 -0
  4. data/Gemfile +1 -0
  5. data/RELEASE_NOTES.md +47 -0
  6. data/Rakefile +0 -1
  7. data/erl_src/riak_kv_test_backend.erl +34 -0
  8. data/lib/riak/client.rb +3 -1
  9. data/lib/riak/client/beefcake/messages.rb +49 -1
  10. data/lib/riak/client/beefcake/object_methods.rb +14 -21
  11. data/lib/riak/client/beefcake_protobuffs_backend.rb +58 -12
  12. data/lib/riak/client/decaying.rb +31 -23
  13. data/lib/riak/client/feature_detection.rb +88 -0
  14. data/lib/riak/client/http_backend.rb +27 -6
  15. data/lib/riak/client/http_backend/configuration.rb +13 -0
  16. data/lib/riak/client/http_backend/object_methods.rb +33 -25
  17. data/lib/riak/client/node.rb +7 -2
  18. data/lib/riak/client/protobuffs_backend.rb +54 -3
  19. data/lib/riak/client/search.rb +2 -2
  20. data/lib/riak/conflict.rb +13 -0
  21. data/lib/riak/locale/en.yml +2 -0
  22. data/lib/riak/map_reduce.rb +1 -1
  23. data/lib/riak/map_reduce/filter_builder.rb +2 -2
  24. data/lib/riak/map_reduce/results.rb +49 -0
  25. data/lib/riak/node/console.rb +17 -16
  26. data/lib/riak/node/generation.rb +9 -0
  27. data/lib/riak/rcontent.rb +168 -0
  28. data/lib/riak/robject.rb +37 -157
  29. data/lib/riak/util/escape.rb +5 -1
  30. data/lib/riak/version.rb +1 -1
  31. data/riak-client.gemspec +37 -5
  32. data/spec/fixtures/multipart-basic-conflict.txt +15 -0
  33. data/spec/fixtures/munchausen.txt +1033 -0
  34. data/spec/integration/riak/cluster_spec.rb +1 -1
  35. data/spec/integration/riak/http_backends_spec.rb +23 -2
  36. data/spec/integration/riak/node_spec.rb +2 -2
  37. data/spec/integration/riak/protobuffs_backends_spec.rb +17 -2
  38. data/spec/integration/riak/test_server_spec.rb +1 -1
  39. data/spec/integration/riak/threading_spec.rb +3 -3
  40. data/spec/riak/beefcake_protobuffs_backend_spec.rb +58 -25
  41. data/spec/riak/escape_spec.rb +3 -0
  42. data/spec/riak/feature_detection_spec.rb +61 -0
  43. data/spec/riak/http_backend/object_methods_spec.rb +4 -13
  44. data/spec/riak/http_backend_spec.rb +6 -5
  45. data/spec/riak/map_reduce_spec.rb +0 -5
  46. data/spec/riak/robject_spec.rb +12 -11
  47. data/spec/spec_helper.rb +3 -1
  48. data/spec/support/riak_test.rb +77 -0
  49. data/spec/support/search_corpus_setup.rb +18 -0
  50. data/spec/support/sometimes.rb +1 -1
  51. data/spec/support/test_server.rb +1 -1
  52. data/spec/support/unified_backend_examples.rb +53 -7
  53. data/spec/support/version_filter.rb +4 -11
  54. metadata +56 -22
  55. data/lib/riak/client/pool.rb +0 -180
  56. data/spec/riak/pool_spec.rb +0 -306
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore CHANGED
@@ -1,3 +1,7 @@
1
+ ## NOTE: This file must be manually kept in sync with the gemspec, but
2
+ ## should not normally need to be modified unless new top-level files
3
+ ## or directory trees are being added.
4
+
1
5
  ## MAC OS
2
6
  .DS_Store
3
7
 
@@ -18,9 +22,6 @@ coverage
18
22
  rdoc
19
23
  pkg
20
24
 
21
- ## This line is for the dirglob only
22
- pkg/*
23
-
24
25
  ## PROJECT::SPECIFIC
25
26
  _notes
26
27
  doc
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile CHANGED
@@ -2,6 +2,7 @@ source :rubygems
2
2
 
3
3
  gemspec
4
4
  gem 'bundler'
5
+ gem 'rake'
5
6
 
6
7
  group :guard do
7
8
  gem 'guard-rspec'
data/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Riak Ruby Client Release Notes
2
2
 
3
+ ## 1.1.0 Feature Release - 2012-11-07
4
+
5
+ Release 1.1.0 includes full Riak 1.2 compatibility, and includes
6
+ improvements to the handling of siblings, the node generation
7
+ tools, and resolves a number of important bugs.
8
+
9
+ Features:
10
+
11
+ * Client features are enabled or disabled based on the detected Riak
12
+ version.
13
+ * Riak 1.2 compatibility, including search and 2I over Protocol
14
+ Buffers.
15
+ * Phaseless MapReduce (which was available in 1.1) is allowed, using
16
+ feature detection to determine whether an exception is raised.
17
+ * Conditional store_object operations on Protocol Buffers use the
18
+ message features available since Riak 1.0.
19
+ * The integration test-suite can be run without generating a test
20
+ node, which lets us support riak_test.
21
+
22
+ Bugfixes:
23
+
24
+ * URL-escaping now allows some normally URI-safe characters to be
25
+ escaped.
26
+ * JRuby should be more reliable when attaching to a generated node's
27
+ console.
28
+ * The client backend pool has been extracted to the Innertube gem,
29
+ which is now a dependency.
30
+ * Fix a documentation issue around key-filters.
31
+ * Fix RSpec formatter and deprecation errors.
32
+ * Object siblings are now a separate class (RContent) rather than
33
+ being unclean copies of the parent RObject. If only one sibling
34
+ exists, the original accessors (e.g. `content_type`, `data`) will
35
+ behave as expected. When more than one sibling exists, they will
36
+ raise `Riak::Conflict`. This should prevent unintentional storing of
37
+ unresolved objects back into Riak as `multipart/mixed` values.
38
+ * `Riak::Client#ssl=` won't blow away existing `ssl_options` if set to
39
+ `true`.
40
+ * Generated nodes will ensure that the source's
41
+ `ssl_distribution.args_file` exists by invoking `riak chkconfig`.
42
+ * Copy fixes for the `$key` index on the memory/test backend from
43
+ riak_kv.
44
+ * The shape of MapReduce results will no longer be changed by the
45
+ Protocol Buffers backend, which manifested as kept phases without
46
+ results being removed from the return value. Implementing this
47
+ required all HTTP requests to use streaming, even if invoked without
48
+ a block.
49
+
3
50
  ## 1.0.5 Packaging Fix Release - 2012-10-12
4
51
 
5
52
  Release 1.0.5 fixes a bug with the RubyGems packaging that
data/Rakefile CHANGED
@@ -69,5 +69,4 @@ desc "Run All Specs (including slow specs)"
69
69
  RSpec::Core::RakeTask.new(:ci) do |spec|
70
70
  spec.rspec_opts = %w[--profile]
71
71
  end
72
-
73
72
  task :default => :ci
@@ -61,6 +61,7 @@
61
61
 
62
62
  -ifdef(TEST).
63
63
  -include_lib("eunit/include/eunit.hrl").
64
+ -compile([export_all]).
64
65
  -endif.
65
66
 
66
67
  -define(API_VERSION, 1).
@@ -427,6 +428,9 @@ fold_keys_fun(FoldKeysFun, {index, FilterBucket, {eq, <<"$bucket">>, _}}) ->
427
428
  fold_keys_fun(FoldKeysFun, {index, FilterBucket, {range, <<"$key">>, _, _}}) ->
428
429
  %% 2I range query on special $key field...
429
430
  fold_keys_fun(FoldKeysFun, {bucket, FilterBucket});
431
+ fold_keys_fun(FoldKeysFun, {index, FilterBucket, {eq, <<"$key">>, _}}) ->
432
+ %% 2I eq query on special $key field...
433
+ fold_keys_fun(FoldKeysFun, {bucket, FilterBucket});
430
434
  fold_keys_fun(FoldKeysFun, {index, _FilterBucket, _Query}) ->
431
435
  fun({{Bucket, _FilterField, _FilterTerm, Key}, _}, Acc) ->
432
436
  FoldKeysFun(Bucket, Key, Acc);
@@ -469,6 +473,8 @@ get_index_folder(Folder, Acc0, {index, Bucket, {range, <<"$key">>, Min, Max}}, D
469
473
  fun() ->
470
474
  key_range_folder(Folder, Acc0, DataRef, {Bucket, Min}, {Bucket, Min, Max})
471
475
  end;
476
+ get_index_folder(Folder, Acc0, {index, Bucket, {eq, <<"$key">>, Val}}, DataRef, IndexRef) ->
477
+ get_index_folder(Folder, Acc0, {index, Bucket, {range, <<"$key">>, Val, Val}}, DataRef, IndexRef);
472
478
  get_index_folder(Folder, Acc0, {index, Bucket, {eq, Field, Term}}, _, IndexRef) ->
473
479
  fun() ->
474
480
  index_range_folder(Folder, Acc0, IndexRef, {Bucket, Field, Term, undefined}, {Bucket, Field, Term, Term})
@@ -668,6 +674,34 @@ max_memory_test_() ->
668
674
  ?_assertEqual({ok, Value2, State2}, get(Bucket, Key2, State2))
669
675
  ].
670
676
 
677
+ regression_367_key_range_test_() ->
678
+ {ok, State} = start(142, []),
679
+ Keys = [begin
680
+ Bin = list_to_binary(integer_to_list(I)),
681
+ if I < 10 ->
682
+ <<"obj0", Bin/binary>>;
683
+ true -> <<"obj", Bin/binary>>
684
+ end
685
+ end || I <- lists:seq(1,30) ],
686
+ Bucket = <<"keyrange">>,
687
+ Value = <<"foobarbaz">>,
688
+ State1 = lists:foldl(fun(Key, IState) ->
689
+ {ok, NewState} = put(Bucket, Key, [], Value, IState),
690
+ NewState
691
+ end, State, Keys),
692
+ Folder = fun(_B, K, Acc) ->
693
+ Acc ++ [K]
694
+ end,
695
+ [
696
+ ?_assertEqual({ok, [<<"obj01">>]}, fold_keys(Folder, [], [{index, Bucket, {range, <<"$key">>, <<"obj01">>, <<"obj01">>}}], State1)),
697
+ ?_assertEqual({ok, [<<"obj10">>,<<"obj11">>]}, fold_keys(Folder, [], [{index, Bucket, {range, <<"$key">>, <<"obj10">>, <<"obj11">>}}], State1)),
698
+ ?_assertEqual({ok, [<<"obj01">>]}, fold_keys(Folder, [], [{index, Bucket, {range, <<"$key">>, <<"obj00">>, <<"obj01">>}}], State1)),
699
+ ?_assertEqual({ok, lists:sort(Keys)}, fold_keys(Folder, [], [{index, Bucket, {range, <<"$key">>, <<"obj0">>, <<"obj31">>}}], State1)),
700
+ ?_assertEqual({ok, []}, fold_keys(Folder, [], [{index, Bucket, {range, <<"$key">>, <<"obj31">>, <<"obj32">>}}], State1)),
701
+ ?_assertEqual({ok, [<<"obj01">>]}, fold_keys(Folder, [], [{index, Bucket, {eq, <<"$key">>, <<"obj01">>}}], State1)),
702
+ ?_assertEqual(ok, stop(State1))
703
+ ].
704
+
671
705
  -ifdef(EQC).
672
706
 
673
707
  eqc_test_() ->
data/lib/riak/client.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  require 'tempfile'
2
2
  require 'delegate'
3
+ require 'innertube'
3
4
  require 'riak'
4
5
  require 'riak/util/translation'
5
6
  require 'riak/util/escape'
6
7
  require 'riak/failed_request'
7
- require 'riak/client/pool'
8
8
  require 'riak/client/decaying'
9
9
  require 'riak/client/node'
10
10
  require 'riak/client/search'
@@ -47,6 +47,8 @@ module Riak
47
47
  SystemCallError,
48
48
  ]
49
49
 
50
+ Pool = ::Innertube::Pool
51
+
50
52
  # @return [String] The protocol to use for the Riak endpoint
51
53
  attr_reader :protocol
52
54
 
@@ -89,7 +89,7 @@ module Riak
89
89
  required :content, RpbContent, 4
90
90
  optional :w, :uint32, 5
91
91
  optional :dw, :uint32, 6
92
- optional :returnbody, :bool, 7
92
+ optional :returnbody, :bool, 7
93
93
  optional :pw, :uint32, 8
94
94
  optional :if_not_modified, :bool, 9
95
95
  optional :if_none_match, :bool, 10
@@ -160,6 +160,54 @@ module Riak
160
160
  optional :response, :bytes, 2
161
161
  optional :done, :bool, 3
162
162
  end
163
+
164
+ class RpbIndexReq
165
+ include Beefcake::Message
166
+ module IndexQueryType
167
+ EQ = 0
168
+ RANGE = 1
169
+ end
170
+
171
+ required :bucket, :bytes, 1
172
+ required :index, :bytes, 2
173
+ required :qtype, IndexQueryType, 3
174
+ optional :key, :bytes, 4
175
+ optional :range_min, :bytes, 5
176
+ optional :range_max, :bytes, 6
177
+ end
178
+
179
+ class RpbIndexResp
180
+ include Beefcake::Message
181
+ repeated :keys, :bytes, 1
182
+ end
183
+
184
+ class RpbSearchDoc
185
+ include Beefcake::Message
186
+ # We have to name this differently than the .proto file does
187
+ # because Beefcake uses 'fields' as an instance method.
188
+ repeated :properties, RpbPair, 1
189
+ end
190
+
191
+ class RpbSearchQueryReq
192
+ include Beefcake::Message
193
+ required :q, :bytes, 1
194
+ required :index, :bytes, 2
195
+ optional :rows, :uint32, 3
196
+ optional :start, :uint32, 4
197
+ optional :sort, :bytes, 5
198
+ optional :filter, :bytes, 6
199
+ optional :df, :bytes, 7
200
+ optional :op, :bytes, 8
201
+ repeated :fl, :bytes, 9
202
+ optional :presort, :bytes, 10
203
+ end
204
+
205
+ class RpbSearchQueryResp
206
+ include Beefcake::Message
207
+ repeated :docs, RpbSearchDoc, 1, :default => []
208
+ optional :max_score, :float, 2
209
+ optional :num_found, :uint32, 3
210
+ end
163
211
  end
164
212
  end
165
213
  end
@@ -31,40 +31,33 @@ module Riak
31
31
  return robject if pbuf.respond_to?(:unchanged) && pbuf.unchanged # Reloading
32
32
  robject.vclock = Base64.encode64(pbuf.vclock).chomp if pbuf.vclock
33
33
  robject.key = maybe_unescape(pbuf.key) if pbuf.respond_to?(:key) && pbuf.key # Put w/o key
34
- if pbuf.content.size > 1
35
- robject.conflict = true
36
- robject.siblings = pbuf.content.map do |c|
37
- sibling = RObject.new(robject.bucket, robject.key)
38
- sibling.vclock = robject.vclock
34
+ robject.siblings = pbuf.content.map do |c|
35
+ RContent.new(robject) do |sibling|
39
36
  load_content(c, sibling)
40
37
  end
41
-
42
- return robject.attempt_conflict_resolution
43
- else
44
- load_content(pbuf.content.first, robject)
45
38
  end
46
- robject
39
+ robject.conflict? ? robject.attempt_conflict_resolution : robject
47
40
  end
48
41
 
49
42
  private
50
- def load_content(pbuf, robject)
43
+ def load_content(pbuf, rcontent)
51
44
  if ENCODING && pbuf.charset.present?
52
45
  pbuf.value.force_encoding(pbuf.charset) if Encoding.find(pbuf.charset)
53
46
  end
54
- robject.raw_data = pbuf.value
55
- robject.etag = pbuf.vtag if pbuf.vtag.present?
56
- robject.content_type = pbuf.content_type if pbuf.content_type.present?
57
- robject.links = pbuf.links.map(&method(:decode_link)) if pbuf.links.present?
58
- pbuf.usermeta.each {|pair| decode_meta(pair, robject.meta) } if pbuf.usermeta.present?
47
+ rcontent.raw_data = pbuf.value
48
+ rcontent.etag = pbuf.vtag if pbuf.vtag.present?
49
+ rcontent.content_type = pbuf.content_type if pbuf.content_type.present?
50
+ rcontent.links = pbuf.links.map(&method(:decode_link)) if pbuf.links.present?
51
+ pbuf.usermeta.each {|pair| decode_meta(pair, rcontent.meta) } if pbuf.usermeta.present?
59
52
  if pbuf.indexes.present?
60
- robject.indexes.clear
61
- pbuf.indexes.each {|pair| decode_index(pair, robject.indexes) }
53
+ rcontent.indexes.clear
54
+ pbuf.indexes.each {|pair| decode_index(pair, rcontent.indexes) }
62
55
  end
63
56
  if pbuf.last_mod.present?
64
- robject.last_modified = Time.at(pbuf.last_mod)
65
- robject.last_modified += pbuf.last_mod_usecs / 1000000 if pbuf.last_mod_usecs.present?
57
+ rcontent.last_modified = Time.at(pbuf.last_mod)
58
+ rcontent.last_modified += pbuf.last_mod_usecs / 1000000 if pbuf.last_mod_usecs.present?
66
59
  end
67
- robject
60
+ rcontent
68
61
  end
69
62
 
70
63
  def decode_link(pbuf)
@@ -31,7 +31,7 @@ module Riak
31
31
  end
32
32
 
33
33
  def fetch_object(bucket, key, options={})
34
- options = normalize_quorums(options)
34
+ options = prune_unsupported_options(:GetReq, normalize_quorums(options))
35
35
  bucket = Bucket === bucket ? bucket.name : bucket
36
36
  req = RpbGetReq.new(options.merge(:bucket => maybe_encode(bucket), :key => maybe_encode(key)))
37
37
  write_protobuff(:GetReq, req)
@@ -43,18 +43,25 @@ module Riak
43
43
  options[:bucket] = maybe_encode(robject.bucket.name)
44
44
  options[:key] = maybe_encode(robject.key)
45
45
  options[:if_modified] = maybe_encode Base64.decode64(robject.vclock) if robject.vclock
46
- req = RpbGetReq.new(options)
46
+ req = RpbGetReq.new(prune_unsupported_options(:GetReq, options))
47
47
  write_protobuff(:GetReq, req)
48
48
  decode_response(robject)
49
49
  end
50
50
 
51
51
  def store_object(robject, options={})
52
+ options = normalize_quorums(options)
52
53
  if robject.prevent_stale_writes
53
- other = fetch_object(robject.bucket, robject.key)
54
- raise Riak::ProtobuffsFailedRequest(:stale_object, t("stale_write_prevented")) unless other.vclock == robject.vclock
54
+ unless pb_conditionals?
55
+ other = fetch_object(robject.bucket, robject.key)
56
+ raise Riak::ProtobuffsFailedRequest.new(:stale_object, t("stale_write_prevented")) unless other.vclock == robject.vclock
57
+ end
58
+ if robject.vclock
59
+ options[:if_not_modified] = true
60
+ else
61
+ options[:if_none_match] = true
62
+ end
55
63
  end
56
- options = normalize_quorums(options)
57
- req = dump_object(robject, options)
64
+ req = dump_object(robject, prune_unsupported_options(:PutReq, options))
58
65
  write_protobuff(:PutReq, req)
59
66
  decode_response(robject)
60
67
  end
@@ -65,7 +72,7 @@ module Riak
65
72
  options[:bucket] = maybe_encode(bucket)
66
73
  options[:key] = maybe_encode(key)
67
74
  options[:vclock] = Base64.decode64(options[:vclock]) if options[:vclock]
68
- req = RpbDelReq.new(options)
75
+ req = RpbDelReq.new(prune_unsupported_options(:DelReq, options))
69
76
  write_protobuff(:DelReq, req)
70
77
  decode_response
71
78
  end
@@ -102,19 +109,47 @@ module Riak
102
109
  end
103
110
 
104
111
  def mapred(mr, &block)
112
+ raise MapReduceError.new(t("empty_map_reduce_query")) if mr.query.empty? && !mapred_phaseless?
105
113
  req = RpbMapRedReq.new(:request => mr.to_json, :content_type => "application/json")
106
114
  write_protobuff(:MapRedReq, req)
107
- results = []
115
+ results = MapReduce::Results.new(mr)
108
116
  while msg = decode_response
109
117
  break if msg.done
110
118
  if block_given?
111
119
  yield msg.phase, JSON.parse(msg.response)
112
120
  else
113
- results[msg.phase] ||= []
114
- results[msg.phase] += JSON.parse(msg.response)
121
+ results.add msg.phase, JSON.parse(msg.response)
115
122
  end
116
123
  end
117
- block_given? || results.compact.size == 1 ? results.last : results
124
+ block_given? || results.report
125
+ end
126
+
127
+ def get_index(bucket, index, query)
128
+ return super unless pb_indexes?
129
+ if Range === query
130
+ options = {
131
+ :qtype => RpbIndexReq::IndexQueryType::RANGE,
132
+ :range_min => query.begin.to_s,
133
+ :range_max => query.end.to_s
134
+ }
135
+ else
136
+ options = {
137
+ :qtype => RpbIndexReq::IndexQueryType::EQ,
138
+ :key => query.to_s
139
+ }
140
+ end
141
+ req = RpbIndexReq.new(options.merge(:bucket => bucket, :index => index))
142
+ write_protobuff(:IndexReq, req)
143
+ decode_response
144
+ end
145
+
146
+ def search(index, query, options={})
147
+ return super unless pb_search?
148
+ options = options.symbolize_keys
149
+ options[:op] = options.delete(:'q.op') if options[:'q.op']
150
+ req = RpbSearchQueryReq.new(options.merge(:index => index || 'search', :q => query))
151
+ write_protobuff(:SearchQueryReq, req)
152
+ decode_response
118
153
  end
119
154
 
120
155
  private
@@ -167,12 +202,23 @@ module Riak
167
202
  {'n_val' => res.props.n_val, 'allow_mult' => res.props.allow_mult}
168
203
  when :MapRedResp
169
204
  RpbMapRedResp.decode(message)
205
+ when :IndexResp
206
+ res = RpbIndexResp.decode(message)
207
+ res.keys
208
+ when :SearchQueryResp
209
+ res = RpbSearchQueryResp.decode(message)
210
+ { 'docs' => res.docs.map {|d| decode_doc(d) },
211
+ 'max_score' => res.max_score,
212
+ 'num_found' => res.num_found }
170
213
  end
171
214
  end
172
215
  rescue SystemCallError, SocketError => e
173
216
  reset_socket
174
217
  raise
175
- #raise Riak::ProtobuffsFailedRequest.new(:server_error, e.message)
218
+ end
219
+
220
+ def decode_doc(doc)
221
+ Hash[doc.properties.map {|p| [ p.key, p.value ] }]
176
222
  end
177
223
  end
178
224
  end
@@ -1,28 +1,36 @@
1
- class Riak::Client::Decaying
2
- # A float which decays exponentially with time.
1
+ module Riak
2
+ class Client
3
+ # A float value which decays exponentially toward 0 over time.
4
+ # @private
5
+ class Decaying
6
+ attr_accessor :e
7
+ attr_accessor :p
3
8
 
4
- attr_accessor :e
5
- attr_accessor :p
6
- # @param[:p] The initial value
7
- # @param[:e] Exponent base
8
- # @param[:r] Timescale
9
- def initialize(opts = {})
10
- @p = opts[:p] || 0
11
- @e = opts[:e] || Math::E
12
- @r = opts[:r] || Math.log(0.5) / 10
13
- @t0 = Time.now
14
- end
9
+ # @param [Hash] opts options
10
+ # @option options [Float] :p (0.0) The initial value
11
+ # @option options [Float] :e (Math::E) Exponent base
12
+ # @option options [Float] :r (Math.log(0.5) / 10) Timescale
13
+ # factor - defaulting to decay 50% every 10 seconds
14
+ def initialize(opts = {})
15
+ @p = opts[:p] || 0.0
16
+ @e = opts[:e] || Math::E
17
+ @r = opts[:r] || Math.log(0.5) / 10
18
+ @t0 = Time.now
19
+ end
15
20
 
16
- # Add d to current value.
17
- def <<(d)
18
- @p = value + d
19
- end
21
+ # Add to current value.
22
+ # @param [Float] d the value to add
23
+ def <<(d)
24
+ @p = value + d
25
+ end
20
26
 
21
- # Return current value
22
- def value
23
- now = Time.now
24
- dt = now - @t0
25
- @t0 = now
26
- @p = @p * (@e ** (@r * dt))
27
+ # @return [Float] the current value (adjusted for the time decay)
28
+ def value
29
+ now = Time.now
30
+ dt = now - @t0
31
+ @t0 = now
32
+ @p = @p * (@e ** (@r * dt))
33
+ end
34
+ end
27
35
  end
28
36
  end