riak-client 1.2.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/Gemfile +1 -7
- data/README.markdown +66 -0
- data/RELEASE_NOTES.md +27 -0
- data/lib/riak/bucket.rb +24 -5
- data/lib/riak/client.rb +42 -7
- data/lib/riak/client/beefcake/message_codes.rb +56 -0
- data/lib/riak/client/beefcake/messages.rb +190 -18
- data/lib/riak/client/beefcake_protobuffs_backend.rb +143 -10
- data/lib/riak/client/feature_detection.rb +26 -1
- data/lib/riak/client/http_backend.rb +58 -9
- data/lib/riak/client/http_backend/bucket_streamer.rb +15 -0
- data/lib/riak/client/http_backend/chunked_json_streamer.rb +42 -0
- data/lib/riak/client/http_backend/configuration.rb +17 -1
- data/lib/riak/client/http_backend/key_streamer.rb +4 -32
- data/lib/riak/client/protobuffs_backend.rb +12 -34
- data/lib/riak/counter.rb +101 -0
- data/lib/riak/index_collection.rb +71 -0
- data/lib/riak/list_buckets.rb +28 -0
- data/lib/riak/locale/en.yml +14 -0
- data/lib/riak/multiget.rb +123 -0
- data/lib/riak/node.rb +2 -0
- data/lib/riak/node/configuration.rb +32 -21
- data/lib/riak/node/defaults.rb +2 -0
- data/lib/riak/node/generation.rb +19 -7
- data/lib/riak/node/version.rb +2 -16
- data/lib/riak/robject.rb +1 -0
- data/lib/riak/secondary_index.rb +67 -0
- data/lib/riak/version.rb +1 -1
- data/riak-client.gemspec +3 -2
- data/spec/integration/riak/counters_spec.rb +51 -0
- data/spec/integration/riak/http_backends_spec.rb +24 -14
- data/spec/integration/riak/node_spec.rb +6 -28
- data/spec/riak/beefcake_protobuffs_backend_spec.rb +84 -0
- data/spec/riak/bucket_spec.rb +55 -5
- data/spec/riak/client_spec.rb +34 -0
- data/spec/riak/counter_spec.rb +122 -0
- data/spec/riak/index_collection_spec.rb +50 -0
- data/spec/riak/list_buckets_spec.rb +41 -0
- data/spec/riak/multiget_spec.rb +76 -0
- data/spec/riak/robject_spec.rb +4 -1
- data/spec/riak/secondary_index_spec.rb +225 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/sometimes.rb +2 -2
- data/spec/support/unified_backend_examples.rb +4 -0
- metadata +41 -47
@@ -37,6 +37,90 @@ describe Riak::Client::BeefcakeProtobuffsBackend do
|
|
37
37
|
backend.list_keys(exp_bucket).should == exp_keys
|
38
38
|
end
|
39
39
|
|
40
|
+
context "secondary index" do
|
41
|
+
before :each do
|
42
|
+
@socket = mock(:socket).as_null_object
|
43
|
+
TCPSocket.stub(:new => @socket)
|
44
|
+
end
|
45
|
+
context 'when streaming' do
|
46
|
+
it "should stream when a block is given" do
|
47
|
+
backend.should_receive(:write_protobuff) do |msg, req|
|
48
|
+
msg.should == :IndexReq
|
49
|
+
req[:stream].should == true
|
50
|
+
end
|
51
|
+
backend.should_receive(:decode_index_response)
|
52
|
+
|
53
|
+
blk = proc{:asdf}
|
54
|
+
|
55
|
+
backend.get_index('bucket', 'words', 'asdf'..'hjkl', &blk)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should send batches of results to the block" do
|
59
|
+
backend.should_receive(:write_protobuff)
|
60
|
+
|
61
|
+
response_sets = [%w{asdf asdg asdh}, %w{gggg gggh gggi}]
|
62
|
+
response_messages = response_sets.map do |s|
|
63
|
+
Riak::Client::BeefcakeProtobuffsBackend::RpbIndexResp.new keys: s
|
64
|
+
end
|
65
|
+
response_messages.last.done = true
|
66
|
+
|
67
|
+
response_chunks = response_messages.map do |m|
|
68
|
+
encoded = m.encode
|
69
|
+
header = [encoded.length + 1, 26].pack 'NC'
|
70
|
+
[header, encoded]
|
71
|
+
end.flatten
|
72
|
+
|
73
|
+
@socket.should_receive(:read).and_return(*response_chunks)
|
74
|
+
|
75
|
+
block_body = mock 'block'
|
76
|
+
block_body.should_receive(:check).with(response_sets.first).once
|
77
|
+
block_body.should_receive(:check).with(response_sets.last).once
|
78
|
+
|
79
|
+
blk = proc {|m| block_body.check m }
|
80
|
+
|
81
|
+
backend.get_index 'bucket', 'words', 'asdf'..'hjkl', &blk
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should return a full batch of results when not streaming" do
|
86
|
+
backend.should_receive(:write_protobuff) do |msg, req|
|
87
|
+
msg.should == :IndexReq
|
88
|
+
req[:stream].should_not be
|
89
|
+
end
|
90
|
+
|
91
|
+
response_message = Riak::Client::BeefcakeProtobuffsBackend::
|
92
|
+
RpbIndexResp.new(
|
93
|
+
keys: %w{asdf asdg asdh}
|
94
|
+
).encode
|
95
|
+
header = [response_message.length + 1, 26].pack 'NC'
|
96
|
+
@socket.should_receive(:read).and_return(header, response_message)
|
97
|
+
|
98
|
+
results = backend.get_index 'bucket', 'words', 'asdf'..'hjkl'
|
99
|
+
results.should == %w{asdf asdg asdh}
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should not crash out when no keys or terms are released" do
|
103
|
+
backend.should_receive(:write_protobuff) do |msg, req|
|
104
|
+
msg.should == :IndexReq
|
105
|
+
req[:stream].should_not be
|
106
|
+
end
|
107
|
+
|
108
|
+
response_message = Riak::Client::BeefcakeProtobuffsBackend::
|
109
|
+
RpbIndexResp.new().encode
|
110
|
+
|
111
|
+
header = [response_message.length + 1, 26].pack 'NC'
|
112
|
+
@socket.should_receive(:read).and_return(header, response_message)
|
113
|
+
|
114
|
+
results = nil
|
115
|
+
fetch = proc do
|
116
|
+
results = backend.get_index 'bucket', 'words', 'asdf'
|
117
|
+
end
|
118
|
+
|
119
|
+
fetch.should_not raise_error
|
120
|
+
results.should == []
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
40
124
|
context "#mapred" do
|
41
125
|
let(:mapred) { Riak::MapReduce.new(client).add('test').map("function(){}").map("function(){}") }
|
42
126
|
|
data/spec/riak/bucket_spec.rb
CHANGED
@@ -16,6 +16,7 @@ describe Riak::Bucket do
|
|
16
16
|
lambda { Riak::Bucket.new("foo") }.should raise_error
|
17
17
|
lambda { Riak::Bucket.new("foo", @client) }.should raise_error
|
18
18
|
lambda { Riak::Bucket.new(@client, "foo") }.should_not raise_error
|
19
|
+
expect { Riak::Bucket.new(@client, '') }.to raise_error(ArgumentError)
|
19
20
|
end
|
20
21
|
|
21
22
|
it "should set the client and name attributes" do
|
@@ -27,12 +28,12 @@ describe Riak::Bucket do
|
|
27
28
|
|
28
29
|
describe "accessing keys" do
|
29
30
|
it "should list the keys" do
|
30
|
-
@backend.should_receive(:list_keys).with(@bucket).and_return(["bar"])
|
31
|
+
@backend.should_receive(:list_keys).with(@bucket, {}).and_return(["bar"])
|
31
32
|
@bucket.keys.should == ["bar"]
|
32
33
|
end
|
33
34
|
|
34
35
|
it "should allow streaming keys through block" do
|
35
|
-
@backend.should_receive(:list_keys).with(@bucket).and_yield([]).and_yield(["bar"]).and_yield(["baz"])
|
36
|
+
@backend.should_receive(:list_keys).with(@bucket, {}).and_yield([]).and_yield(["bar"]).and_yield(["baz"])
|
36
37
|
all_keys = []
|
37
38
|
@bucket.keys do |list|
|
38
39
|
all_keys.concat(list)
|
@@ -41,7 +42,7 @@ describe Riak::Bucket do
|
|
41
42
|
end
|
42
43
|
|
43
44
|
it "should not cache the list of keys" do
|
44
|
-
@backend.should_receive(:list_keys).with(@bucket).twice.and_return(["bar"])
|
45
|
+
@backend.should_receive(:list_keys).with(@bucket, {}).twice.and_return(["bar"])
|
45
46
|
2.times { @bucket.keys.should == ['bar'] }
|
46
47
|
end
|
47
48
|
|
@@ -52,6 +53,24 @@ describe Riak::Bucket do
|
|
52
53
|
@bucket.keys
|
53
54
|
Riak.disable_list_keys_warnings = true
|
54
55
|
end
|
56
|
+
|
57
|
+
it "should allow a specified timeout when listing keys" do
|
58
|
+
@backend.should_receive(:list_keys).with(@bucket, timeout: 1234).and_return(%w{bar})
|
59
|
+
|
60
|
+
keys = @bucket.keys timeout: 1234
|
61
|
+
|
62
|
+
keys.should == %w{bar}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "accessing a counter" do
|
67
|
+
it "should return a counter object" do
|
68
|
+
Riak::Counter.should_receive(:new).with(@bucket, 'asdf').and_return('example counter')
|
69
|
+
|
70
|
+
new_counter = @bucket.counter 'asdf'
|
71
|
+
|
72
|
+
new_counter.should == 'example counter'
|
73
|
+
end
|
55
74
|
end
|
56
75
|
|
57
76
|
describe "setting the bucket properties" do
|
@@ -104,6 +123,12 @@ describe Riak::Bucket do
|
|
104
123
|
@backend.should_receive(:fetch_object).with(@bucket, "db", {:r => 2}).and_return(nil)
|
105
124
|
@bucket.get("db", :r => 2)
|
106
125
|
end
|
126
|
+
|
127
|
+
it "should disallow fetching an object with a zero-length key" do
|
128
|
+
## TODO: This actually tests the Client object, but there is no suite
|
129
|
+
## of tests for its generic interface.
|
130
|
+
expect { @bucket.get('') }.to raise_error(ArgumentError)
|
131
|
+
end
|
107
132
|
end
|
108
133
|
|
109
134
|
describe "creating a new blank object" do
|
@@ -141,10 +166,35 @@ describe Riak::Bucket do
|
|
141
166
|
end
|
142
167
|
end
|
143
168
|
|
169
|
+
describe "fetching multiple objects" do
|
170
|
+
it 'should get each object individually' do
|
171
|
+
@object1 = mock('obj1')
|
172
|
+
@object2 = mock('obj2')
|
173
|
+
@bucket.should_receive(:[]).with('key1').and_return(@object1)
|
174
|
+
@bucket.should_receive(:[]).with('key2').and_return(@object2)
|
175
|
+
|
176
|
+
@results = @bucket.get_many %w{key1 key2}
|
177
|
+
|
178
|
+
@results['key1'].should == @object1
|
179
|
+
@results['key2'].should == @object2
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
144
183
|
describe "querying an index" do
|
145
184
|
it "should list the matching keys" do
|
146
|
-
@backend.
|
147
|
-
|
185
|
+
@backend.
|
186
|
+
should_receive(:get_index).
|
187
|
+
with(@bucket, "test_bin", "testing", {return_terms: true}).
|
188
|
+
and_return(Riak::IndexCollection.new_from_json({
|
189
|
+
'results' => [
|
190
|
+
{'testing' => 'asdf'},
|
191
|
+
{'testing' => 'hjkl'}]
|
192
|
+
}.to_json))
|
193
|
+
result = @bucket.get_index("test_bin", "testing", return_terms: true)
|
194
|
+
|
195
|
+
result.should be_a Riak::IndexCollection
|
196
|
+
result.to_a.should == %w{asdf hjkl}
|
197
|
+
result.with_terms.should == {'testing' => %w{asdf hjkl}}
|
148
198
|
end
|
149
199
|
end
|
150
200
|
|
data/spec/riak/client_spec.rb
CHANGED
@@ -196,6 +196,29 @@ describe Riak::Client do
|
|
196
196
|
end
|
197
197
|
end
|
198
198
|
|
199
|
+
describe "retrieving many values" do
|
200
|
+
before :each do
|
201
|
+
@client = Riak::Client.new
|
202
|
+
@bucket = @client.bucket('foo')
|
203
|
+
@bucket.should_receive(:[]).with('value1').and_return(mock('robject'))
|
204
|
+
@bucket.should_receive(:[]).with('value2').and_return(mock('robject'))
|
205
|
+
@pairs = [
|
206
|
+
[@bucket, 'value1'],
|
207
|
+
[@bucket, 'value2']
|
208
|
+
]
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'should accept an array of bucket and key pairs' do
|
212
|
+
lambda{ @client.get_many(@pairs) }.should_not raise_error
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'should return a hash of bucket/key pairs and robjects' do
|
216
|
+
@results = @client.get_many(@pairs)
|
217
|
+
@results.should be_a Hash
|
218
|
+
@results.length.should be(@pairs.length)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
199
222
|
describe "retrieving a bucket" do
|
200
223
|
before :each do
|
201
224
|
@client = Riak::Client.new
|
@@ -218,6 +241,10 @@ describe Riak::Client do
|
|
218
241
|
@client.bucket("baz").should == @bucket
|
219
242
|
@client.bucket("baz").should == @bucket
|
220
243
|
end
|
244
|
+
|
245
|
+
it "should reject buckets with zero-length names" do
|
246
|
+
expect { @client.bucket('') }.to raise_error(ArgumentError)
|
247
|
+
end
|
221
248
|
end
|
222
249
|
|
223
250
|
describe "listing buckets" do
|
@@ -244,6 +271,13 @@ describe Riak::Client do
|
|
244
271
|
@client.should_receive(:warn)
|
245
272
|
@client.buckets
|
246
273
|
end
|
274
|
+
|
275
|
+
it "should support a timeout option" do
|
276
|
+
@backend.should_receive(:list_buckets).with(timeout: 1234).and_return(%w{test test2})
|
277
|
+
|
278
|
+
buckets = @client.buckets timeout: 1234
|
279
|
+
buckets.should have(2).items
|
280
|
+
end
|
247
281
|
end
|
248
282
|
|
249
283
|
describe "Luwak (large-files) support" do
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Riak::Counter do
|
4
|
+
describe "initialization" do
|
5
|
+
before :each do
|
6
|
+
@bucket = Riak::Bucket.allocate
|
7
|
+
@key = 'key'
|
8
|
+
@bucket.stub allow_mult: true
|
9
|
+
@bucket.stub(client: mock('client'))
|
10
|
+
@bucket.stub('is_a?' => true)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should set the bucket and key" do
|
14
|
+
ctr = Riak::Counter.new @bucket, @key
|
15
|
+
ctr.bucket.should == @bucket
|
16
|
+
ctr.key.should == @key
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should require allow_mult" do
|
20
|
+
@bad_bucket = Riak::Bucket.allocate
|
21
|
+
@bad_bucket.stub allow_mult: false
|
22
|
+
@bad_bucket.stub(client: mock('client'))
|
23
|
+
|
24
|
+
expect{ctr = Riak::Counter.new @bad_bucket, @key}.to raise_error(ArgumentError)
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "incrementing and decrementing" do
|
30
|
+
before :each do
|
31
|
+
@backend = mock 'backend'
|
32
|
+
|
33
|
+
@client = mock 'client'
|
34
|
+
@client.stub(:backend).and_yield @backend
|
35
|
+
|
36
|
+
@bucket = Riak::Bucket.allocate
|
37
|
+
@bucket.stub allow_mult: true
|
38
|
+
@bucket.stub client: @client
|
39
|
+
|
40
|
+
@key = 'key'
|
41
|
+
|
42
|
+
@ctr = Riak::Counter.new @bucket, @key
|
43
|
+
|
44
|
+
@increment_expectation = proc{|n| @backend.should_receive(:post_counter).with(@bucket, @key, n, {})}
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should increment by 1 by default" do
|
48
|
+
@increment_expectation[1]
|
49
|
+
@ctr.increment
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should support incrementing by positive numbers" do
|
53
|
+
@increment_expectation[15]
|
54
|
+
@ctr.increment 15
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should support incrementing by negative numbers" do
|
58
|
+
@increment_expectation[-12]
|
59
|
+
@ctr.increment -12
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should decrement by 1 by default" do
|
63
|
+
@increment_expectation[-1]
|
64
|
+
@ctr.decrement
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should support decrementing by positive numbers" do
|
68
|
+
@increment_expectation[-30]
|
69
|
+
@ctr.decrement 30
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should support decrementing by negative numbers" do
|
73
|
+
@increment_expectation[41]
|
74
|
+
@ctr.decrement -41
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should forbid incrementing by non-integers" do
|
78
|
+
[1.1, nil, :'1', '1', 2.0/2, [1]].each do |candidate|
|
79
|
+
expect do
|
80
|
+
@ctr.increment candidate
|
81
|
+
raise candidate.to_s
|
82
|
+
end.to raise_error(ArgumentError)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "failure modes" do
|
88
|
+
before :each do
|
89
|
+
@nodes = 10.times.map do |n|
|
90
|
+
{pb_port: "100#{n}7"}
|
91
|
+
end
|
92
|
+
|
93
|
+
@fake_pool = mock 'pool'
|
94
|
+
@backend = mock 'backend'
|
95
|
+
|
96
|
+
@client = Riak::Client.new nodes: @nodes, protocol: 'pbc'
|
97
|
+
@client.instance_variable_set :@protobuffs_pool, @fake_pool
|
98
|
+
|
99
|
+
@fake_pool.stub(:take).and_yield(@backend)
|
100
|
+
|
101
|
+
@bucket = Riak::Bucket.allocate
|
102
|
+
@bucket.stub allow_mult: true
|
103
|
+
@bucket.stub client: @client
|
104
|
+
|
105
|
+
@key = 'key'
|
106
|
+
|
107
|
+
@expect_post = @backend.should_receive(:post_counter).with(@bucket, @key, 1, {})
|
108
|
+
|
109
|
+
@ctr = Riak::Counter.new @bucket, @key
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should not retry on timeout" do
|
113
|
+
@expect_post.once.and_raise('timeout')
|
114
|
+
expect(proc { @ctr.increment }).to raise_error
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should not retry on quorum failure" do
|
118
|
+
@expect_post.once.and_raise('quorum not satisfied')
|
119
|
+
expect(proc { @ctr.increment }).to raise_error
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Riak::IndexCollection do
|
4
|
+
describe "json initialization" do
|
5
|
+
it "should accept a list of keys" do
|
6
|
+
@input = {
|
7
|
+
'keys' => %w{first second third}
|
8
|
+
}.to_json
|
9
|
+
lambda { @coll = Riak::IndexCollection.new_from_json @input }.should_not raise_error
|
10
|
+
%w{first second third}.should == @coll
|
11
|
+
end
|
12
|
+
it "should accept a list of keys and a continuation" do
|
13
|
+
@input = {
|
14
|
+
'keys' => %w{first second third},
|
15
|
+
'continuation' => 'examplecontinuation'
|
16
|
+
}.to_json
|
17
|
+
lambda { @coll = Riak::IndexCollection.new_from_json @input }.should_not raise_error
|
18
|
+
%w{first second third}.should == @coll
|
19
|
+
@coll.continuation.should == 'examplecontinuation'
|
20
|
+
end
|
21
|
+
it "should accept a list of results hashes" do
|
22
|
+
@input = {
|
23
|
+
'results' => [
|
24
|
+
{'first' => 'first'},
|
25
|
+
{'second' => 'second'},
|
26
|
+
{'second' => 'other'}
|
27
|
+
]
|
28
|
+
}.to_json
|
29
|
+
|
30
|
+
lambda { @coll = Riak::IndexCollection.new_from_json @input }.should_not raise_error
|
31
|
+
%w{first second other}.should == @coll
|
32
|
+
{'first' => %w{first}, 'second' => %w{second other}}.should == @coll.with_terms
|
33
|
+
end
|
34
|
+
it "should accept a list of results hashes and a continuation" do
|
35
|
+
@input = {
|
36
|
+
'results' => [
|
37
|
+
{'first' => 'first'},
|
38
|
+
{'second' => 'second'},
|
39
|
+
{'second' => 'other'}
|
40
|
+
],
|
41
|
+
'continuation' => 'examplecontinuation'
|
42
|
+
}.to_json
|
43
|
+
|
44
|
+
lambda { @coll = Riak::IndexCollection.new_from_json @input }.should_not raise_error
|
45
|
+
%w{first second other}.should == @coll
|
46
|
+
@coll.continuation.should == 'examplecontinuation'
|
47
|
+
{'first' => %w{first}, 'second' => %w{second other}}.should == @coll.with_terms
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Riak::ListBuckets do
|
4
|
+
before :each do
|
5
|
+
@client = Riak::Client.new protocol: 'pbc'
|
6
|
+
@backend = mock 'backend'
|
7
|
+
@fake_pool = mock 'connection pool'
|
8
|
+
@fake_pool.stub(:take).and_yield(@backend)
|
9
|
+
|
10
|
+
@expect_list = @backend.should_receive(:list_buckets)
|
11
|
+
|
12
|
+
@client.instance_variable_set :@protobuffs_pool, @fake_pool
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "non-streaming" do
|
16
|
+
it 'should call the backend without a block' do
|
17
|
+
@expect_list.with({}).and_return(%w{a b c d})
|
18
|
+
|
19
|
+
@client.list_buckets
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "streaming" do
|
24
|
+
it 'should call the backend with a block' do
|
25
|
+
@expect_list.
|
26
|
+
and_yield(%w{abc abd abe}).
|
27
|
+
and_yield(%w{bbb ccc ddd})
|
28
|
+
|
29
|
+
@yielded = []
|
30
|
+
|
31
|
+
@client.list_buckets do |bucket|
|
32
|
+
@yielded << bucket
|
33
|
+
end
|
34
|
+
|
35
|
+
@yielded.each do |b|
|
36
|
+
b.should be_a Riak::Bucket
|
37
|
+
end
|
38
|
+
@yielded.map(&:name).should == %w{abc abd abe bbb ccc ddd}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|