splitclient-rb 3.0.3 → 3.1.0.pre.rc2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.txt +3 -0
- data/NEWS +3 -0
- data/README.md +166 -48
- data/exe/splitio +37 -0
- data/lib/cache/adapters/memory_adapter.rb +65 -16
- data/lib/cache/adapters/redis_adapter.rb +95 -0
- data/lib/cache/repositories/repository.rb +5 -11
- data/lib/cache/repositories/segments_repository.rb +22 -12
- data/lib/cache/repositories/splits_repository.rb +29 -20
- data/lib/cache/stores/split_store.rb +1 -1
- data/lib/engine/parser/split_adapter.rb +25 -14
- data/lib/splitclient-rb.rb +1 -1
- data/lib/splitclient-rb/split_config.rb +30 -11
- data/lib/splitclient-rb/split_factory.rb +9 -16
- data/lib/splitclient-rb/version.rb +1 -1
- data/splitclient-rb.gemspec +1 -0
- metadata +23 -7
- data/lib/cache/adapters/adapter.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 848d0aacbc266ce731699e451bd82976446240e0
|
4
|
+
data.tar.gz: 3de885436b94b499290e29a0aac4ca1f1767e660
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 550e6f8a4e3bf0b475946055fe432ce1a3790035170811a131270e25ee8dee6ebdfc70ce6baedd9797f07d56cea9cc1904fa23fe4dff630c3823463dd8fe3cf1
|
7
|
+
data.tar.gz: cc9e7d82c6878c698614de9b092a8ca705f884127213c9359bd1aafbeabd3abe20b08760d50e52cdba1d14b0013b65d76467993e3c3ab1ef21e20b34fe0bcb70
|
data/CHANGES.txt
CHANGED
data/NEWS
CHANGED
data/README.md
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
#
|
1
|
+
# Split Ruby SDK
|
2
2
|
|
3
|
-
Ruby
|
3
|
+
Ruby SDK for Split software, provided as a gem that can be installed to your Ruby application.
|
4
4
|
|
5
5
|
## Installation
|
6
|
-
|
6
|
+
---
|
7
7
|
|
8
8
|
- Once the gem is published you can install it with the following steps:
|
9
9
|
|
@@ -15,30 +15,32 @@ Ruby client for split software. This is provided as a gem that can be installed
|
|
15
15
|
|
16
16
|
And then execute:
|
17
17
|
|
18
|
-
$ bundle
|
18
|
+
$ bundle install
|
19
19
|
|
20
20
|
Or install it yourself as:
|
21
21
|
|
22
22
|
$ gem install splitclient-rb
|
23
23
|
|
24
|
-
-
|
24
|
+
- You can also use the most recent version from github:
|
25
25
|
|
26
|
-
Add these lines to you application's
|
26
|
+
Add these lines to you application's `Gemfile`:
|
27
27
|
```ruby
|
28
|
-
gem 'splitclient-rb', :
|
28
|
+
gem 'splitclient-rb', git: 'https://github.com/splitio/ruby-client.git',
|
29
29
|
```
|
30
|
-
You can also
|
30
|
+
You can also use any specific branch if necessary:
|
31
31
|
```ruby
|
32
|
-
gem 'splitclient-rb', :
|
32
|
+
gem 'splitclient-rb', git: 'https://github.com/splitio/ruby-client.git', branch: 'development'
|
33
33
|
```
|
34
34
|
And then execute:
|
35
35
|
|
36
36
|
$ bundle install
|
37
37
|
|
38
38
|
## Usage
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
|
40
|
+
### Quick Setup
|
41
|
+
---
|
42
|
+
|
43
|
+
Within your application you need the following:
|
42
44
|
|
43
45
|
Require the Split client:
|
44
46
|
```ruby
|
@@ -47,7 +49,7 @@ require 'splitclient-rb'
|
|
47
49
|
|
48
50
|
Create a new split client instance with your API key:
|
49
51
|
```ruby
|
50
|
-
factory = SplitIoClient::SplitFactory.new(
|
52
|
+
factory = SplitIoClient::SplitFactory.new('YOUR_API_KEY').client
|
51
53
|
split_client = factory.client
|
52
54
|
```
|
53
55
|
|
@@ -56,67 +58,101 @@ For advance use cases you can also obtain a `manager` instance from the factory.
|
|
56
58
|
manager = factory.manager
|
57
59
|
```
|
58
60
|
|
59
|
-
###Ruby on Rails
|
60
|
-
|
61
|
-
If you're using Ruby on Rails
|
61
|
+
### Ruby on Rails
|
62
|
+
---
|
62
63
|
|
63
|
-
Create an initializer
|
64
|
+
Create an initializer: `config/initializers/splitclient.rb` and then initialize the split client:
|
64
65
|
```ruby
|
65
|
-
Rails.configuration.split_client = SplitIoClient::SplitFactory.new(
|
66
|
+
Rails.configuration.split_client = SplitIoClient::SplitFactory.new('YOUR_API_KEY').client
|
66
67
|
```
|
67
|
-
In your controllers, access the client using
|
68
|
+
In your controllers, access the client using:
|
68
69
|
|
69
70
|
```ruby
|
70
71
|
Rails.application.config.split_client
|
71
72
|
```
|
72
73
|
|
73
|
-
###Configuration
|
74
|
+
### Configuration
|
74
75
|
---
|
75
|
-
By default the split client uses its default configuration, it will be sufficient for most scenarios. However you can also provide custom configuration when initializing the client using an optional hash of options.
|
76
76
|
|
77
|
-
|
77
|
+
Split client's default configuration should be sufficient for most scenarios. However you can also provide custom configuration when initializing the client using an optional hash of options.
|
78
|
+
|
79
|
+
The following values can be customized:
|
78
80
|
|
79
81
|
**base_uri** : URI for the api endpoints
|
80
|
-
*defualt value* : https://sdk.split.io/api/
|
81
82
|
|
82
|
-
|
83
|
-
*default value* : custom cache local storage
|
83
|
+
*defualt value* = `https://sdk.split.io/api/`
|
84
84
|
|
85
85
|
**connection_timeout** : timeout for network connections in seconds
|
86
|
-
|
86
|
+
|
87
|
+
*default value* = `5`
|
87
88
|
|
88
89
|
**read_timeout** : timeout for requests in seconds
|
89
|
-
|
90
|
+
|
91
|
+
*default value* = `5`
|
90
92
|
|
91
93
|
**features_refresh_rate** : The SDK polls Split servers for changes to feature roll-out plans. This parameter controls this polling period in seconds
|
92
|
-
|
94
|
+
|
95
|
+
*default value* = `30`
|
93
96
|
|
94
97
|
**segments_refresh_rate** : The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds
|
95
|
-
|
98
|
+
|
99
|
+
*default value* = `60`
|
96
100
|
|
97
101
|
**metrics_refresh_rate** : The SDK sends diagnostic metrics to Split servers. This parameters controls this metric flush period in seconds
|
98
|
-
|
102
|
+
|
103
|
+
*default value* = `60`
|
99
104
|
|
100
105
|
**impressions_refresh_rate** : The SDK sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers in seconds
|
101
|
-
|
106
|
+
|
107
|
+
*default value* = `60`
|
102
108
|
|
103
109
|
**debug_enabled** : Enables extra logging
|
104
|
-
|
110
|
+
|
111
|
+
*default value* = `false`
|
105
112
|
|
106
113
|
**transport_debug_enabled** : Enables extra transport logging
|
107
|
-
|
114
|
+
|
115
|
+
*default value* = `false`
|
108
116
|
|
109
117
|
**logger** : default logger for messages and errors
|
110
|
-
|
118
|
+
|
119
|
+
*default value* = `Logger.new($stdout)`
|
111
120
|
|
112
121
|
**block_until_ready** : The SDK will block your app for provided amount of seconds until it's ready. If timeout expires `SplitIoClient::SDKBlockerTimeoutExpiredException` will be thrown. If `false` provided, then SDK would run in non-blocking mode
|
113
|
-
|
122
|
+
|
123
|
+
*default value* = `false`
|
124
|
+
|
125
|
+
**mode** : See [SDK modes section](#sdk-modes).
|
126
|
+
|
127
|
+
*default value* = `:standalone`
|
128
|
+
|
129
|
+
#### Cache adapter
|
130
|
+
|
131
|
+
The SDK needs some container to store fetched data, i.e. splits/segments. By default it will store everything in the application's memory, but you can also use Redis.
|
132
|
+
|
133
|
+
To use Redis, you have to include `redis-rb` in your app's Gemfile.
|
134
|
+
|
135
|
+
**cache_adapter** : Supported options: `:memory`, `:redis`
|
136
|
+
|
137
|
+
*default value* = `memory`
|
138
|
+
|
139
|
+
**redis_url** : Redis URL or hash with configuration for SDK to connect to.
|
140
|
+
|
141
|
+
*default value* = `'redis://127.0.0.1:6379/0'`
|
142
|
+
|
143
|
+
You can also use Sentinel like this:
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
SENTINELS = [{host: '127.0.0.1', port: 26380},
|
147
|
+
{host: '127.0.0.1', port: 26381}]
|
148
|
+
|
149
|
+
redis_url = Redis.new(url: 'redis://mymaster', sentinels: SENTINELS, role: :master)
|
150
|
+
```
|
114
151
|
|
115
152
|
Example
|
116
153
|
```ruby
|
117
154
|
options = {
|
118
155
|
base_uri: 'https://my.app.api/',
|
119
|
-
local_store: Rails.cache,
|
120
156
|
connection_timeout: 10,
|
121
157
|
read_timeout: 5,
|
122
158
|
features_refresh_rate: 120,
|
@@ -124,51 +160,62 @@ options = {
|
|
124
160
|
metrics_refresh_rate: 360,
|
125
161
|
impressions_refresh_rate: 360,
|
126
162
|
logger: Logger.new('logfile.log'),
|
127
|
-
block_until_ready: 5
|
163
|
+
block_until_ready: 5,
|
164
|
+
cache_adapter: :redis,
|
165
|
+
redis_url: 'redis://127.0.0.1:6379/0'
|
128
166
|
}
|
129
167
|
begin
|
130
|
-
split_client = SplitIoClient::SplitFactory.new(
|
168
|
+
split_client = SplitIoClient::SplitFactory.new('YOUR_API_KEY', options).client
|
131
169
|
rescue SplitIoClient::SDKBlockerTimeoutExpiredException
|
132
170
|
# Some arbitrary actions
|
133
171
|
end
|
134
172
|
```
|
135
|
-
This begin-rescue-end block is optional, you might want to use it to catch timeout expired exception and apply some logic
|
173
|
+
This begin-rescue-end block is optional, you might want to use it to catch timeout expired exception and apply some logic.
|
136
174
|
|
137
175
|
### Execution
|
138
176
|
---
|
139
|
-
|
177
|
+
|
178
|
+
In your application code you just need to call the `get_treatment` method with the required parameters for key and feature name:
|
140
179
|
```ruby
|
141
|
-
split_client.get_treatment('user_id','feature_name',
|
180
|
+
split_client.get_treatment('user_id','feature_name', attr: 'val')
|
142
181
|
```
|
143
182
|
|
144
183
|
For example
|
145
184
|
```ruby
|
146
|
-
if split_client.get_treatment('employee_user_01','view_main_list',
|
185
|
+
if split_client.get_treatment('employee_user_01','view_main_list', age: 35)
|
147
186
|
my_app.display_main_list
|
148
187
|
end
|
149
188
|
```
|
150
189
|
|
151
190
|
Also, you can use different keys for actually getting treatment and sending impressions to the server:
|
152
191
|
```ruby
|
153
|
-
split_client.get_treatment(
|
192
|
+
split_client.get_treatment(
|
193
|
+
{ matching_key: 'user_id', bucketing_key: 'private_user_id' },
|
194
|
+
'feature_name',
|
195
|
+
attr: 'val'
|
196
|
+
)
|
154
197
|
```
|
155
198
|
When it might be useful? Say, you have a user browsing your website and not signed up yet. You assign some internal id to that user (i.e. bucketing_key) and after user signs up you assign him a matching_key.
|
156
199
|
By doing this you can provide both anonymous and signed up user with the same treatment.
|
157
200
|
|
158
201
|
`bucketing_key` may be `nil` in that case `matching_key` would be used as a key, so calling
|
159
202
|
```ruby
|
160
|
-
split_client.get_treatment(
|
203
|
+
split_client.get_treatment(
|
204
|
+
{ matching_key: 'user_id' },
|
205
|
+
'feature_name',
|
206
|
+
attr: 'val'
|
207
|
+
)
|
161
208
|
```
|
162
209
|
Is exactly the same as calling
|
163
210
|
```ruby
|
164
|
-
split_client.get_treatment('user_id' ,'feature_name',
|
211
|
+
split_client.get_treatment('user_id' ,'feature_name', attr: 'val')
|
165
212
|
```
|
166
213
|
`bucketing_key` must not be nil
|
167
214
|
|
168
215
|
Also you can use the split manager:
|
169
216
|
|
170
217
|
```ruby
|
171
|
-
split_manager = SplitIoClient::SplitFactory.new(
|
218
|
+
split_manager = SplitIoClient::SplitFactory.new('your_api_key', options).manager
|
172
219
|
```
|
173
220
|
|
174
221
|
With the manager you can get a list of your splits by doing:
|
@@ -179,10 +226,81 @@ manager.splits
|
|
179
226
|
|
180
227
|
And you should get something like this:
|
181
228
|
|
182
|
-
```
|
183
|
-
|
229
|
+
```ruby
|
230
|
+
[
|
231
|
+
{
|
232
|
+
name: 'some_feature',
|
233
|
+
traffic_type_name: nil,
|
234
|
+
killed: false,
|
235
|
+
treatments: nil,
|
236
|
+
change_number: 1469134003507
|
237
|
+
},
|
238
|
+
{
|
239
|
+
name: 'another_feature',
|
240
|
+
traffic_type_name: nil,
|
241
|
+
killed: false,
|
242
|
+
treatments: nil,
|
243
|
+
change_number: 1469134003414
|
244
|
+
},
|
245
|
+
{
|
246
|
+
name: 'even_more_features',
|
247
|
+
traffic_type_name: nil,
|
248
|
+
killed: false,
|
249
|
+
treatments: nil,
|
250
|
+
change_number: 1469133991063
|
251
|
+
},
|
252
|
+
{
|
253
|
+
name: 'yet_another_feature',
|
254
|
+
traffic_type_name: nil,
|
255
|
+
killed: false,
|
256
|
+
treatments: nil,
|
257
|
+
change_number: 1469133757521
|
258
|
+
}
|
259
|
+
]
|
184
260
|
```
|
185
261
|
|
262
|
+
### SDK Modes
|
263
|
+
|
264
|
+
By default SDK would run alongside with your application and will be run in `standalone` mode, which includes two modes:
|
265
|
+
- `producer` - storing information from the Splits API in the chosen cache
|
266
|
+
- `consumer` - retrieving data from the cache and providing `get_treatment` interface
|
267
|
+
|
268
|
+
As you might think, you can choose between these 3 modes by providing `mode` option in the config.
|
269
|
+
|
270
|
+
#### Producer mode
|
271
|
+
|
272
|
+
If you have, say, one Redis cache which is used by several Split SDKs at once, e.g.: Python and Ruby, you want to have only one of them to write data to Redis, so it would remain consistent. That's why we have producer mode.
|
273
|
+
|
274
|
+
SDK can be ran in `producer` mode both in the scope of the application (e.g. as a part of the Rails app), and as a separate process. Let's see what steps are needed to run it as a separate process:
|
275
|
+
|
276
|
+
- You need to create a config file with .yml extension. All options specified in the above example section are valid, but you should write them in the YAML format, like this:
|
277
|
+
|
278
|
+
```yaml
|
279
|
+
---
|
280
|
+
:api_key: 'SECRET_API_KEY'
|
281
|
+
:base_uri: 'https://my.app.api/'
|
282
|
+
:connection_timeout: 10
|
283
|
+
:read_timeout: 5
|
284
|
+
:features_refresh_rate: 120
|
285
|
+
:segments_refresh_rate: 120
|
286
|
+
:metrics_refresh_rate: 360
|
287
|
+
:impressions_refresh_rate: 360
|
288
|
+
:block_until_ready: 5
|
289
|
+
:cache_adapter: :redis
|
290
|
+
:redis_url: 'redis://127.0.0.1:6379/0'
|
291
|
+
```
|
292
|
+
|
293
|
+
- Install binstubs
|
294
|
+
```ruby
|
295
|
+
bundle binstubs splitclient-rb
|
296
|
+
```
|
297
|
+
|
298
|
+
- Run the executable provided by the SDK:
|
299
|
+
```ruby
|
300
|
+
bundle exec bin/splitio -c ~/path/to/config/file.yml
|
301
|
+
```
|
302
|
+
|
303
|
+
That's it!
|
186
304
|
|
187
305
|
## Development
|
188
306
|
|
data/exe/splitio
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
lib = File.expand_path('../../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require 'optparse'
|
7
|
+
require 'yaml'
|
8
|
+
require_relative '../lib/splitclient-rb'
|
9
|
+
|
10
|
+
ARGV << '-h' if ARGV.empty?
|
11
|
+
|
12
|
+
options = {}
|
13
|
+
opt_parser = OptionParser.new do |opts|
|
14
|
+
opts.banner = "Usage: splitio [options]"
|
15
|
+
|
16
|
+
opts.on("-cPATH", "--config=PATH", "Set the path to splitio.yml config file") do |c|
|
17
|
+
options[:config_path] = c
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on_tail("-h", "--help", "Prints this help") do
|
21
|
+
puts opts
|
22
|
+
exit
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
begin
|
27
|
+
opt_parser.parse!(ARGV)
|
28
|
+
rescue OptionParser::InvalidOption => e
|
29
|
+
puts e
|
30
|
+
puts opt_parser
|
31
|
+
exit(1)
|
32
|
+
end
|
33
|
+
|
34
|
+
config = YAML.load_file(options[:config_path])
|
35
|
+
config.merge!(mode: :producer, cache_adapter: :redis)
|
36
|
+
|
37
|
+
SplitIoClient::SplitFactory.new(config[:api_key], config)
|
@@ -3,42 +3,91 @@ require 'concurrent'
|
|
3
3
|
module SplitIoClient
|
4
4
|
module Cache
|
5
5
|
module Adapters
|
6
|
-
class MemoryAdapter
|
6
|
+
class MemoryAdapter
|
7
7
|
def initialize
|
8
8
|
@map = Concurrent::Map.new
|
9
9
|
end
|
10
10
|
|
11
11
|
# Map
|
12
|
-
def
|
12
|
+
def initialize_map(key)
|
13
|
+
@map[key] = Concurrent::Map.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_to_map(key, field, value)
|
17
|
+
@map[key].put(field, value)
|
18
|
+
end
|
19
|
+
|
20
|
+
def find_in_map(key, field)
|
21
|
+
return nil if @map[key].nil?
|
22
|
+
|
23
|
+
@map[key].get(field)
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete_from_map(key, fields)
|
27
|
+
if fields.is_a? Array
|
28
|
+
fields.each { |field| @map[key].delete(field) }
|
29
|
+
else
|
30
|
+
@map[key].delete(field)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def in_map?(key, field)
|
35
|
+
return false if @map[key].nil?
|
36
|
+
|
37
|
+
@map[key].key?(field)
|
38
|
+
end
|
39
|
+
|
40
|
+
def map_keys(key)
|
41
|
+
@map[key].keys
|
42
|
+
end
|
43
|
+
|
44
|
+
def get_map(key)
|
13
45
|
@map[key]
|
14
46
|
end
|
15
47
|
|
16
|
-
|
17
|
-
|
48
|
+
# String
|
49
|
+
def string(key)
|
50
|
+
@map[key]
|
18
51
|
end
|
19
52
|
|
20
|
-
def
|
21
|
-
@map[key] =
|
53
|
+
def set_string(key, str)
|
54
|
+
@map[key] = str
|
22
55
|
end
|
23
56
|
|
24
|
-
def
|
25
|
-
@map
|
57
|
+
def find_strings_by_prefix(prefix)
|
58
|
+
@map.keys.select { |str| str.start_with? prefix }
|
26
59
|
end
|
27
60
|
|
28
|
-
|
29
|
-
|
61
|
+
# Bool
|
62
|
+
def set_bool(key, val)
|
63
|
+
@map[key] = val
|
64
|
+
end
|
30
65
|
|
31
|
-
|
66
|
+
def bool(key)
|
67
|
+
@map[key]
|
32
68
|
end
|
33
69
|
|
34
|
-
|
35
|
-
|
70
|
+
# Set
|
71
|
+
alias_method :initialize_set, :initialize_map
|
72
|
+
alias_method :get_set, :map_keys
|
73
|
+
alias_method :delete_from_set, :delete_from_map
|
74
|
+
alias_method :in_set?, :in_map?
|
75
|
+
|
76
|
+
def add_to_set(key, values)
|
77
|
+
if values.is_a? Array
|
78
|
+
values.each { |value| add_to_map(key, value, 1) }
|
79
|
+
else
|
80
|
+
add_to_map(key, values, 1)
|
81
|
+
end
|
36
82
|
end
|
37
83
|
|
38
|
-
|
39
|
-
|
84
|
+
# General
|
85
|
+
def exists?(key)
|
86
|
+
!@map[key].nil?
|
87
|
+
end
|
40
88
|
|
41
|
-
|
89
|
+
def delete(key)
|
90
|
+
@map[key] = nil
|
42
91
|
end
|
43
92
|
end
|
44
93
|
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module SplitIoClient
|
5
|
+
module Cache
|
6
|
+
module Adapters
|
7
|
+
class RedisAdapter
|
8
|
+
def initialize(redis_url)
|
9
|
+
connection = redis_url.is_a?(Hash) ? redis_url : { url: redis_url }
|
10
|
+
|
11
|
+
@redis = Redis.new(connection)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Map
|
15
|
+
def initialize_map(key)
|
16
|
+
# No need to initialize hash/map in Redis
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_to_map(key, field, value)
|
20
|
+
@redis.hset(key, field, value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_in_map(key, field)
|
24
|
+
@redis.hget(key, field)
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete_from_map(key, field)
|
28
|
+
@redis.hdel(key, field)
|
29
|
+
end
|
30
|
+
|
31
|
+
def in_map?(key, field)
|
32
|
+
@redis.hexists(key, field)
|
33
|
+
end
|
34
|
+
|
35
|
+
def map_keys(key)
|
36
|
+
@redis.hkeys(key)
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_map(key)
|
40
|
+
@redis.hgetall(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
# String
|
44
|
+
def string(key)
|
45
|
+
@redis.get(key)
|
46
|
+
end
|
47
|
+
|
48
|
+
def set_string(key, str)
|
49
|
+
@redis.set(key, str)
|
50
|
+
end
|
51
|
+
|
52
|
+
def find_strings_by_prefix(prefix)
|
53
|
+
@redis.keys("#{prefix}*")
|
54
|
+
end
|
55
|
+
|
56
|
+
# Bool
|
57
|
+
def set_bool(key, val)
|
58
|
+
@redis.set(key, val.to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
def bool(key)
|
62
|
+
@redis.get(key) == 'true'
|
63
|
+
end
|
64
|
+
|
65
|
+
# Set
|
66
|
+
alias_method :initialize_set, :initialize_map
|
67
|
+
|
68
|
+
def add_to_set(key, val)
|
69
|
+
@redis.sadd(key, val)
|
70
|
+
end
|
71
|
+
|
72
|
+
def delete_from_set(key, val)
|
73
|
+
@redis.srem(key, val)
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_set(key)
|
77
|
+
@redis.smembers(key)
|
78
|
+
end
|
79
|
+
|
80
|
+
def in_set?(key, val)
|
81
|
+
@redis.sismember(key, val)
|
82
|
+
end
|
83
|
+
|
84
|
+
# General
|
85
|
+
def exists?(key)
|
86
|
+
@redis.exists(key)
|
87
|
+
end
|
88
|
+
|
89
|
+
def delete(key)
|
90
|
+
@redis.del(key)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -1,24 +1,18 @@
|
|
1
1
|
module SplitIoClient
|
2
2
|
module Cache
|
3
3
|
class Repository
|
4
|
-
def
|
5
|
-
@adapter
|
6
|
-
|
7
|
-
@adapter[namespace_key('ready')] = false
|
8
|
-
end
|
9
|
-
|
10
|
-
def []=(key, obj)
|
11
|
-
@adapter[namespace_key(key)] = obj
|
4
|
+
def set_string(key, str)
|
5
|
+
@adapter.set_string(namespace_key(key), str)
|
12
6
|
end
|
13
7
|
|
14
|
-
def
|
15
|
-
@adapter
|
8
|
+
def string(key)
|
9
|
+
@adapter.string(namespace_key(key))
|
16
10
|
end
|
17
11
|
|
18
12
|
protected
|
19
13
|
|
20
14
|
def namespace_key(key)
|
21
|
-
"
|
15
|
+
"SPLITIO.#{key}"
|
22
16
|
end
|
23
17
|
end
|
24
18
|
end
|
@@ -2,49 +2,59 @@ module SplitIoClient
|
|
2
2
|
module Cache
|
3
3
|
module Repositories
|
4
4
|
class SegmentsRepository < Repository
|
5
|
+
KEYS_SLICE = 3000
|
6
|
+
|
7
|
+
def initialize(adapter)
|
8
|
+
@adapter = adapter
|
9
|
+
|
10
|
+
@adapter.set_bool(namespace_key('ready'), false)
|
11
|
+
end
|
12
|
+
|
5
13
|
def add_to_segment(segment)
|
6
14
|
name = segment[:name]
|
7
15
|
|
8
|
-
@adapter.
|
16
|
+
@adapter.initialize_set(segment_data(name)) unless @adapter.exists?(segment_data(name))
|
9
17
|
|
10
18
|
add_keys(name, segment[:added])
|
11
19
|
remove_keys(name, segment[:removed])
|
12
20
|
end
|
13
21
|
|
14
22
|
def get_segment_keys(name)
|
15
|
-
@adapter
|
23
|
+
@adapter.get_set(segment_data(name))
|
16
24
|
end
|
17
25
|
|
18
26
|
def in_segment?(name, key)
|
19
|
-
@adapter.
|
27
|
+
@adapter.in_set?(segment_data(name), key)
|
20
28
|
end
|
21
29
|
|
22
30
|
def used_segment_names
|
23
|
-
@adapter
|
31
|
+
@adapter.get_set(namespace_key('segments.registered'))
|
24
32
|
end
|
25
33
|
|
26
34
|
def set_change_number(name, last_change)
|
27
|
-
@adapter.
|
28
|
-
|
29
|
-
@adapter.add_to_map(namespace_key('changes'), name, last_change)
|
35
|
+
@adapter.set_string(namespace_key("segment.#{name}.till"), last_change)
|
30
36
|
end
|
31
37
|
|
32
38
|
def get_change_number(name)
|
33
|
-
@adapter.
|
39
|
+
@adapter.string(namespace_key("segment.#{name}.till")) || -1
|
34
40
|
end
|
35
41
|
|
36
42
|
private
|
37
43
|
|
38
|
-
def
|
39
|
-
"
|
44
|
+
def segment_data(name)
|
45
|
+
namespace_key("segmentData.#{name}")
|
40
46
|
end
|
41
47
|
|
42
48
|
def add_keys(name, keys)
|
43
|
-
keys.
|
49
|
+
keys.each_slice(KEYS_SLICE) do |keys_slice|
|
50
|
+
@adapter.add_to_set(segment_data(name), keys_slice)
|
51
|
+
end
|
44
52
|
end
|
45
53
|
|
46
54
|
def remove_keys(name, keys)
|
47
|
-
keys.
|
55
|
+
keys.each_slice(KEYS_SLICE) do |keys_slice|
|
56
|
+
@adapter.delete_from_set(segment_data(name), keys_slice)
|
57
|
+
end
|
48
58
|
end
|
49
59
|
end
|
50
60
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'concurrent'
|
2
|
+
|
1
3
|
module SplitIoClient
|
2
4
|
module Cache
|
3
5
|
module Repositories
|
@@ -5,50 +7,57 @@ module SplitIoClient
|
|
5
7
|
def initialize(adapter)
|
6
8
|
@adapter = adapter
|
7
9
|
|
8
|
-
@adapter
|
9
|
-
@adapter.initialize_map(namespace_key('
|
10
|
-
@adapter.initialize_map(namespace_key('used_segment_names'))
|
10
|
+
@adapter.set_string(namespace_key('split.till'), '-1')
|
11
|
+
@adapter.initialize_map(namespace_key('segments.registered'))
|
11
12
|
end
|
12
13
|
|
13
14
|
def add_split(split)
|
14
|
-
|
15
|
-
|
16
|
-
@adapter.add_to_map(namespace_key('splits'), split[:name], split_without_name)
|
15
|
+
@adapter.set_string(namespace_key("split.#{split[:name]}"), split.to_json)
|
17
16
|
end
|
18
17
|
|
19
18
|
def remove_split(name)
|
20
|
-
@adapter.
|
19
|
+
@adapter.delete(namespace_key("split.#{name}"))
|
21
20
|
end
|
22
21
|
|
23
|
-
def get_split(name)
|
24
|
-
@adapter.
|
22
|
+
def get_split(name, prefixed = false)
|
23
|
+
split = prefixed ? @adapter.string(name) : @adapter.string(namespace_key("split.#{name}"))
|
24
|
+
|
25
|
+
JSON.parse(split, symbolize_names: true)
|
25
26
|
end
|
26
27
|
|
27
|
-
def
|
28
|
-
|
28
|
+
def splits
|
29
|
+
splits_hash = {}
|
30
|
+
splits = []
|
31
|
+
split_names = @adapter.find_strings_by_prefix(namespace_key('split'))
|
32
|
+
|
33
|
+
split_names.each do |name|
|
34
|
+
next if name == namespace_key('split.till')
|
35
|
+
|
36
|
+
splits << get_split(name, true)
|
37
|
+
end
|
38
|
+
|
39
|
+
splits.each do |split|
|
40
|
+
splits_hash[split[:name]] = split
|
41
|
+
end
|
42
|
+
|
43
|
+
splits_hash
|
29
44
|
end
|
30
45
|
|
31
46
|
def set_change_number(since)
|
32
|
-
@adapter
|
47
|
+
@adapter.set_string(namespace_key('split.till'), since)
|
33
48
|
end
|
34
49
|
|
35
50
|
def get_change_number
|
36
|
-
@adapter
|
51
|
+
@adapter.string(namespace_key('split.till'))
|
37
52
|
end
|
38
53
|
|
39
54
|
def set_segment_names(names)
|
40
55
|
return if names.nil? || names.empty?
|
41
56
|
|
42
57
|
names.each do |name|
|
43
|
-
@adapter.
|
58
|
+
@adapter.add_to_set(namespace_key('segments.registered'), name)
|
44
59
|
end
|
45
60
|
end
|
46
|
-
|
47
|
-
private
|
48
|
-
|
49
|
-
def namespace_key(key)
|
50
|
-
"splits_repository_#{key}"
|
51
|
-
end
|
52
61
|
end
|
53
62
|
end
|
54
63
|
end
|
@@ -40,7 +40,6 @@ module SplitIoClient
|
|
40
40
|
#
|
41
41
|
# @return [SplitIoClient] split.io client instance
|
42
42
|
def initialize(api_key, config, splits_repository, segments_repository, sdk_blocker)
|
43
|
-
|
44
43
|
@api_key = api_key
|
45
44
|
@config = config
|
46
45
|
@impressions = Impressions.new(100)
|
@@ -56,11 +55,25 @@ module SplitIoClient
|
|
56
55
|
builder.adapter :net_http_persistent
|
57
56
|
end
|
58
57
|
|
59
|
-
@
|
60
|
-
|
61
|
-
@metrics_producer = create_metrics_api_producer
|
62
|
-
@impressions_producer = create_impressions_api_producer
|
58
|
+
start_based_on_mode(@config.mode)
|
59
|
+
end
|
63
60
|
|
61
|
+
def start_based_on_mode(mode)
|
62
|
+
case mode
|
63
|
+
when :standalone
|
64
|
+
split_store
|
65
|
+
segment_store
|
66
|
+
metrics_sender
|
67
|
+
impressions_sender
|
68
|
+
when :consumer
|
69
|
+
metrics_sender
|
70
|
+
impressions_sender
|
71
|
+
when :producer
|
72
|
+
split_store
|
73
|
+
segment_store
|
74
|
+
|
75
|
+
sleep unless ENV['SPLITCLIENT_ENV'] == 'test'
|
76
|
+
end
|
64
77
|
end
|
65
78
|
|
66
79
|
#
|
@@ -69,11 +82,11 @@ module SplitIoClient
|
|
69
82
|
# provided within the configuration
|
70
83
|
#
|
71
84
|
# @return [void]
|
72
|
-
def
|
85
|
+
def split_store
|
73
86
|
SplitIoClient::Cache::Stores::SplitStore.new(@splits_repository, @config, @api_key, @metrics, @sdk_blocker).call
|
74
87
|
end
|
75
88
|
|
76
|
-
def
|
89
|
+
def segment_store
|
77
90
|
SplitIoClient::Cache::Stores::SegmentStore.new(@segments_repository, @config, @api_key, @metrics, @sdk_blocker).call
|
78
91
|
end
|
79
92
|
|
@@ -116,11 +129,10 @@ module SplitIoClient
|
|
116
129
|
# provided within the configuration
|
117
130
|
#
|
118
131
|
|
119
|
-
def
|
132
|
+
def metrics_sender
|
120
133
|
Thread.new do
|
121
134
|
loop do
|
122
135
|
begin
|
123
|
-
#post captured metrics
|
124
136
|
post_metrics
|
125
137
|
|
126
138
|
random_interval = randomize_interval @config.metrics_refresh_rate
|
@@ -132,11 +144,10 @@ module SplitIoClient
|
|
132
144
|
end
|
133
145
|
end
|
134
146
|
|
135
|
-
def
|
147
|
+
def impressions_sender
|
136
148
|
Thread.new do
|
137
149
|
loop do
|
138
150
|
begin
|
139
|
-
#post captured impressions
|
140
151
|
post_impressions
|
141
152
|
|
142
153
|
random_interval = randomize_interval @config.impressions_refresh_rate
|
@@ -161,14 +172,14 @@ module SplitIoClient
|
|
161
172
|
test_impression_array = []
|
162
173
|
popped_impressions.each do |i|
|
163
174
|
filtered = []
|
164
|
-
|
175
|
+
keys_treatments_seen = []
|
165
176
|
|
166
177
|
impressions = i[:impressions]
|
167
178
|
impressions.each do |imp|
|
168
|
-
if
|
179
|
+
if keys_treatments_seen.include?("#{imp.key}:#{imp.treatment}")
|
169
180
|
next
|
170
181
|
end
|
171
|
-
|
182
|
+
keys_treatments_seen << "#{imp.key}:#{imp.treatment}"
|
172
183
|
filtered << imp
|
173
184
|
end
|
174
185
|
|
data/lib/splitclient-rb.rb
CHANGED
@@ -5,8 +5,8 @@ require 'splitclient-rb/localhost_split_factory_builder'
|
|
5
5
|
require 'splitclient-rb/localhost_split_factory'
|
6
6
|
require 'splitclient-rb/split_config'
|
7
7
|
require 'exceptions/sdk_blocker_timeout_expired_exception'
|
8
|
-
require 'cache/adapters/adapter'
|
9
8
|
require 'cache/adapters/memory_adapter'
|
9
|
+
require 'cache/adapters/redis_adapter'
|
10
10
|
require 'cache/repositories/repository'
|
11
11
|
require 'cache/repositories/segments_repository'
|
12
12
|
require 'cache/repositories/splits_repository'
|
@@ -15,7 +15,6 @@ module SplitIoClient
|
|
15
15
|
# @option opts [String] :events_uri ("https://events.split.io/api/") The events URL for events end points
|
16
16
|
# @option opts [Int] :read_timeout (10) The read timeout for network connections in seconds.
|
17
17
|
# @option opts [Int] :connection_timeout (2) The connect timeout for network connections in seconds.
|
18
|
-
# @option opts [Object] :local_store A cache store for the Faraday HTTP caching library. Defaults to the Rails cache in a Rails environment, or a thread-safe in-memory store otherwise.
|
19
18
|
# @option opts [Int] :features_refresh_rate The SDK polls Split servers for changes to feature roll-out plans. This parameter controls this polling period in seconds.
|
20
19
|
# @option opts [Int] :segments_refresh_rate
|
21
20
|
# @option opts [Int] :metrics_refresh_rate
|
@@ -27,7 +26,9 @@ module SplitIoClient
|
|
27
26
|
def initialize(opts = {})
|
28
27
|
@base_uri = (opts[:base_uri] || SplitConfig.default_base_uri).chomp('/')
|
29
28
|
@events_uri = (opts[:events_uri] || SplitConfig.default_events_uri).chomp('/')
|
30
|
-
@
|
29
|
+
@mode = opts[:mode] || SplitConfig.default_mode
|
30
|
+
@redis_url = opts[:redis_url] || SplitConfig.default_redis_url
|
31
|
+
@cache_adapter = SplitConfig.init_cache_adapter(opts[:cache_adapter] || SplitConfig.default_cache_adapter, @redis_url)
|
31
32
|
@connection_timeout = opts[:connection_timeout] || SplitConfig.default_connection_timeout
|
32
33
|
@read_timeout = opts[:read_timeout] || SplitConfig.default_read_timeout
|
33
34
|
@features_refresh_rate = opts[:features_refresh_rate] || SplitConfig.default_features_refresh_rate
|
@@ -41,7 +42,7 @@ module SplitIoClient
|
|
41
42
|
@machine_name = SplitConfig.get_hostname
|
42
43
|
@machine_ip = SplitConfig.get_ip
|
43
44
|
|
44
|
-
|
45
|
+
startup_log
|
45
46
|
end
|
46
47
|
|
47
48
|
#
|
@@ -57,13 +58,11 @@ module SplitIoClient
|
|
57
58
|
attr_reader :events_uri
|
58
59
|
|
59
60
|
#
|
60
|
-
# The
|
61
|
-
# 'read', 'write' and 'delete' requests.
|
61
|
+
# The mode SDK will run
|
62
62
|
#
|
63
|
-
# @return [
|
64
|
-
attr_reader :
|
63
|
+
# @return [Symbol] One of the available SDK modes: standalone, consumer, producer
|
64
|
+
attr_reader :mode
|
65
65
|
|
66
|
-
#
|
67
66
|
# The read timeout for network connections in seconds.
|
68
67
|
#
|
69
68
|
# @return [Int] The timeout in seconds.
|
@@ -114,6 +113,8 @@ module SplitIoClient
|
|
114
113
|
attr_reader :metrics_refresh_rate
|
115
114
|
attr_reader :impressions_refresh_rate
|
116
115
|
|
116
|
+
attr_reader :redis_url
|
117
|
+
|
117
118
|
#
|
118
119
|
# The default split client configuration
|
119
120
|
#
|
@@ -134,9 +135,22 @@ module SplitIoClient
|
|
134
135
|
'https://events.split.io/api/'
|
135
136
|
end
|
136
137
|
|
138
|
+
def self.init_cache_adapter(adapter, redis_url)
|
139
|
+
case adapter
|
140
|
+
when :memory
|
141
|
+
SplitIoClient::Cache::Adapters::MemoryAdapter.new
|
142
|
+
when :redis
|
143
|
+
SplitIoClient::Cache::Adapters::RedisAdapter.new(redis_url)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.default_mode
|
148
|
+
:standalone
|
149
|
+
end
|
150
|
+
|
137
151
|
# @return [LocalStore] configuration value for local cache store
|
138
152
|
def self.default_cache_adapter
|
139
|
-
|
153
|
+
:memory
|
140
154
|
end
|
141
155
|
|
142
156
|
#
|
@@ -187,6 +201,10 @@ module SplitIoClient
|
|
187
201
|
false
|
188
202
|
end
|
189
203
|
|
204
|
+
def self.default_redis_url
|
205
|
+
'redis://127.0.0.1:6379/0'
|
206
|
+
end
|
207
|
+
|
190
208
|
#
|
191
209
|
# The default transport_debug_enabled value
|
192
210
|
#
|
@@ -206,10 +224,11 @@ module SplitIoClient
|
|
206
224
|
end
|
207
225
|
|
208
226
|
#
|
209
|
-
# log which cache class was loaded
|
227
|
+
# log which cache class was loaded and SDK mode
|
210
228
|
#
|
211
229
|
# @return [void]
|
212
|
-
def
|
230
|
+
def startup_log
|
231
|
+
@logger.info("Loaded SDK in the #{@mode} mode")
|
213
232
|
@logger.info("Loaded cache class: #{@cache_adapter.class}")
|
214
233
|
end
|
215
234
|
|
@@ -49,19 +49,12 @@ module SplitIoClient
|
|
49
49
|
#
|
50
50
|
# @returns [object] array of splits
|
51
51
|
def splits
|
52
|
-
|
53
52
|
return @localhost_mode_features if @localhost_mode
|
54
|
-
return
|
55
|
-
|
56
|
-
splits = @splits_repository.list_splits
|
57
|
-
ret = []
|
58
|
-
splits.keys.each do |key|
|
59
|
-
|
60
|
-
split = splits.get(key)
|
61
|
-
ret << build_split_view(key, split) if !Engine::Parser::SplitTreatment.archived?(split)
|
62
|
-
end
|
53
|
+
return if @splits_repository.nil?
|
63
54
|
|
64
|
-
|
55
|
+
@splits_repository.splits.each_with_object([]) do |(name, split), memo|
|
56
|
+
memo << build_split_view(name, split) unless Engine::Parser::SplitTreatment.archived?(split)
|
57
|
+
end
|
65
58
|
end
|
66
59
|
|
67
60
|
#
|
@@ -75,8 +68,8 @@ module SplitIoClient
|
|
75
68
|
end
|
76
69
|
|
77
70
|
if @splits_repository
|
78
|
-
|
79
|
-
split = @splits_repository.get_split(split_name)
|
71
|
+
|
72
|
+
split = @splits_repository.get_split(split_name)
|
80
73
|
|
81
74
|
build_split_view(split_name, split) if split and !Engine::Parser::SplitTreatment.archived?(split)
|
82
75
|
end
|
@@ -169,9 +162,9 @@ module SplitIoClient
|
|
169
162
|
begin
|
170
163
|
@adapter.impressions.log(matching_key, feature, result, (Time.now.to_f * 1000.0))
|
171
164
|
latency = (Time.now - start) * 1000.0
|
172
|
-
if (@adapter.impressions.queue.length >= @adapter.impressions.max_number_of_keys)
|
173
|
-
|
174
|
-
end
|
165
|
+
# if (@adapter.impressions.queue.length >= @adapter.impressions.max_number_of_keys)
|
166
|
+
# @adapter.impressions_producer.wakeup
|
167
|
+
# end
|
175
168
|
rescue StandardError => error
|
176
169
|
@config.log_found_exception(__method__.to_s, error)
|
177
170
|
end
|
data/splitclient-rb.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: splitclient-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0.
|
4
|
+
version: 3.1.0.pre.rc2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Split Software
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-10-
|
11
|
+
date: 2016-10-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -220,10 +220,25 @@ dependencies:
|
|
220
220
|
- - "<="
|
221
221
|
- !ruby/object:Gem::Version
|
222
222
|
version: 2.9.4
|
223
|
+
- !ruby/object:Gem::Dependency
|
224
|
+
name: redis
|
225
|
+
requirement: !ruby/object:Gem::Requirement
|
226
|
+
requirements:
|
227
|
+
- - "~>"
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: '3.2'
|
230
|
+
type: :runtime
|
231
|
+
prerelease: false
|
232
|
+
version_requirements: !ruby/object:Gem::Requirement
|
233
|
+
requirements:
|
234
|
+
- - "~>"
|
235
|
+
- !ruby/object:Gem::Version
|
236
|
+
version: '3.2'
|
223
237
|
description: Ruby client for using split SDK.
|
224
238
|
email:
|
225
239
|
- pato@split.io
|
226
|
-
executables:
|
240
|
+
executables:
|
241
|
+
- splitio
|
227
242
|
extensions: []
|
228
243
|
extra_rdoc_files: []
|
229
244
|
files:
|
@@ -234,8 +249,9 @@ files:
|
|
234
249
|
- NEWS
|
235
250
|
- README.md
|
236
251
|
- Rakefile
|
237
|
-
-
|
252
|
+
- exe/splitio
|
238
253
|
- lib/cache/adapters/memory_adapter.rb
|
254
|
+
- lib/cache/adapters/redis_adapter.rb
|
239
255
|
- lib/cache/repositories/repository.rb
|
240
256
|
- lib/cache/repositories/segments_repository.rb
|
241
257
|
- lib/cache/repositories/splits_repository.rb
|
@@ -293,12 +309,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
293
309
|
version: '0'
|
294
310
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
295
311
|
requirements:
|
296
|
-
- - "
|
312
|
+
- - ">"
|
297
313
|
- !ruby/object:Gem::Version
|
298
|
-
version:
|
314
|
+
version: 1.3.1
|
299
315
|
requirements: []
|
300
316
|
rubyforge_project:
|
301
|
-
rubygems_version: 2.5.
|
317
|
+
rubygems_version: 2.5.2
|
302
318
|
signing_key:
|
303
319
|
specification_version: 4
|
304
320
|
summary: Ruby client for split SDK.
|
@@ -1,23 +0,0 @@
|
|
1
|
-
module SplitIoClient
|
2
|
-
module Cache
|
3
|
-
module Adapters
|
4
|
-
class Adapter
|
5
|
-
def set
|
6
|
-
raise NoMethodError
|
7
|
-
end
|
8
|
-
|
9
|
-
def get
|
10
|
-
raise NoMethodError
|
11
|
-
end
|
12
|
-
|
13
|
-
def remove
|
14
|
-
raise NoMethodError
|
15
|
-
end
|
16
|
-
|
17
|
-
def key?
|
18
|
-
raise NoMethodError
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|