splitclient-rb 5.1.0.pre.rc1-java → 5.1.1-java
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 +4 -4
- data/.gitignore +3 -0
- data/.rubocop.yml +9 -5
- data/CHANGES.txt +12 -1
- data/Detailed-README.md +1 -1
- data/NEWS +10 -2
- data/exe/splitio +6 -6
- data/lib/splitclient-rb/cache/repositories/events/memory_repository.rb +3 -4
- data/lib/splitclient-rb/cache/repositories/events/redis_repository.rb +1 -2
- data/lib/splitclient-rb/cache/repositories/events_repository.rb +6 -7
- data/lib/splitclient-rb/cache/repositories/impressions/memory_repository.rb +6 -7
- data/lib/splitclient-rb/cache/repositories/impressions/redis_repository.rb +9 -10
- data/lib/splitclient-rb/cache/repositories/impressions_repository.rb +3 -4
- data/lib/splitclient-rb/cache/repositories/metrics/memory_repository.rb +1 -3
- data/lib/splitclient-rb/cache/repositories/metrics/redis_repository.rb +1 -3
- data/lib/splitclient-rb/cache/repositories/metrics_repository.rb +3 -4
- data/lib/splitclient-rb/cache/repositories/repository.rb +2 -2
- data/lib/splitclient-rb/cache/repositories/segments_repository.rb +2 -4
- data/lib/splitclient-rb/cache/repositories/splits_repository.rb +12 -16
- data/lib/splitclient-rb/cache/routers/impression_router.rb +4 -5
- data/lib/splitclient-rb/cache/senders/events_sender.rb +6 -7
- data/lib/splitclient-rb/cache/senders/impressions_sender.rb +8 -9
- data/lib/splitclient-rb/cache/senders/metrics_sender.rb +6 -7
- data/lib/splitclient-rb/cache/stores/sdk_blocker.rb +3 -4
- data/lib/splitclient-rb/cache/stores/segment_store.rb +11 -12
- data/lib/splitclient-rb/cache/stores/split_store.rb +12 -13
- data/lib/splitclient-rb/clients/localhost_split_client.rb +2 -2
- data/lib/splitclient-rb/clients/split_client.rb +20 -19
- data/lib/splitclient-rb/engine/api/client.rb +22 -22
- data/lib/splitclient-rb/engine/api/events.rb +6 -8
- data/lib/splitclient-rb/engine/api/impressions.rb +5 -6
- data/lib/splitclient-rb/engine/api/metrics.rb +8 -9
- data/lib/splitclient-rb/engine/api/segments.rb +2 -3
- data/lib/splitclient-rb/engine/api/splits.rb +2 -3
- data/lib/splitclient-rb/engine/metrics/metrics.rb +1 -4
- data/lib/splitclient-rb/engine/parser/split_adapter.rb +8 -10
- data/lib/splitclient-rb/managers/split_manager.rb +1 -2
- data/lib/splitclient-rb/split_config.rb +47 -38
- data/lib/splitclient-rb/split_factory.rb +13 -14
- data/lib/splitclient-rb/split_logger.rb +3 -8
- data/lib/splitclient-rb/version.rb +1 -1
- data/splitclient-rb.gemspec +1 -1
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8903ab793f89c27a21ae3d916d68029978b22f72
|
4
|
+
data.tar.gz: 29606cfe1e0736b2060c53facc78142d78849374
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8bbca21009723c69445733989995a376ee8e8de059af13ec2a1cc4a4f970a01383f9a39dbba701848a7c3332615f13586eb1d78db5c510cdb5f1df82166bbbcf
|
7
|
+
data.tar.gz: a8874df6c0dc553648cd2dcfc10f78b678f2e21f0a3e21026b1df0906e1fe2f00e22aab03a21878940554b3b628178ba9416f667029d72a31171b9d4b0488f3d
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -5,11 +5,15 @@ Metrics/LineLength:
|
|
5
5
|
Max: 121
|
6
6
|
|
7
7
|
Metrics/BlockLength:
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
Exclude:
|
9
|
+
- spec/**/*
|
10
|
+
- splitclient-rb.gemspec
|
11
|
+
- exe/splitio
|
12
|
+
|
13
|
+
Naming/FileName:
|
14
|
+
Exclude:
|
15
|
+
- splitclient-rb.gemspec
|
12
16
|
|
13
17
|
AllCops:
|
14
18
|
Exclude:
|
15
|
-
|
19
|
+
- lib/**/* #TODO Apply rubocop to the library
|
data/CHANGES.txt
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
5.1.1 (October 4th, 2018)
|
2
|
+
- Change get_treatments so that it sends a single latency metric
|
3
|
+
- Removed unused call to Redis#scan when adding latencies
|
4
|
+
- Removed Redis calls on initialization when SDK is set to consumer mode
|
5
|
+
- Change split_config approach so that every property has an accessor
|
6
|
+
- Removed @config parameter on most initializers
|
7
|
+
|
8
|
+
5.1.0 (September 10th, 2018)
|
9
|
+
- Change `get_api` to return only a Faraday response.
|
10
|
+
- Add `SplitLogger` to clean up logging code and reduce the complexity in several methods.
|
11
|
+
|
1
12
|
5.0.3 (August 13th, 2018)
|
2
13
|
- Add `impressions_bulk_size` option to set the max number of impressions to be sent to the Split backend on each post.
|
3
14
|
|
@@ -8,7 +19,7 @@
|
|
8
19
|
- Adds stop! method to the factory for gracefully stopping the SDK.
|
9
20
|
|
10
21
|
5.0.0 (May 18th, 2018)
|
11
|
-
- Fix bug where the sdk picked the wrong hashing algo. This is a breaking change.
|
22
|
+
- Fix bug where the sdk picked the wrong hashing algo. This is a breaking change.
|
12
23
|
|
13
24
|
4.5.2 (May 16th, 2018)
|
14
25
|
- do not return control when a split has custom attr and I don't pass attributes to get_treatment
|
data/Detailed-README.md
CHANGED
@@ -468,7 +468,7 @@ The gem uses `rspec` for unit testing. You can find the files for the unit tests
|
|
468
468
|
To run all the specs in the `spec` folder, use the provided rake task (_make sure Redis is running in localhost_):
|
469
469
|
|
470
470
|
```bash
|
471
|
-
|
471
|
+
bundle exec rspec
|
472
472
|
```
|
473
473
|
|
474
474
|
`Simplecov` is used for coverage reporting. Upon executing the rake task it will store the reports in the `/coverage` folder.
|
data/NEWS
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
5.1.1
|
2
|
+
|
3
|
+
Reduces the number of calls to Redis when calling #client.get_treatments using such cache adapter.
|
4
|
+
|
5
|
+
5.1.0
|
6
|
+
|
7
|
+
Prevent unhandled exceptions from raising when API get calls fail on Segments and Treatments.
|
8
|
+
|
1
9
|
5.0.3
|
2
10
|
|
3
11
|
Creates a new configuration parameter (impressions_bulk_size) to manage the max number of impressions sent to the Split backend on each post.
|
@@ -11,13 +19,13 @@ Prevents the impression thread from being started if a listener is not in place
|
|
11
19
|
|
12
20
|
5.0.1
|
13
21
|
|
14
|
-
Adding stop! method to the factory.
|
22
|
+
Adding stop! method to the factory.
|
15
23
|
With this method the user will be able to stop the threads that queries the Split Service. This will be used in the before_fork configuration in Puma and Unicorn to stop the threads in the master process.
|
16
24
|
|
17
25
|
5.0.0
|
18
26
|
|
19
27
|
This is a breaking change in how users buckets are allocated.
|
20
|
-
Ruby SDK was wrongly picking up the right buckets and defauled to
|
28
|
+
Ruby SDK was wrongly picking up the right buckets and defauled to
|
21
29
|
use a "legacy hashing" instead of our moder murmur3 algo which is
|
22
30
|
what other SDKs use. Please reach out to support@split.io for help
|
23
31
|
on how to upgrade to this release.
|
data/exe/splitio
CHANGED
@@ -78,12 +78,12 @@ opt_parser = OptionParser.new do |opts|
|
|
78
78
|
end
|
79
79
|
|
80
80
|
begin
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
81
|
+
opt_parser.parse!(ARGV)
|
82
|
+
rescue OptionParser::InvalidOption => e
|
83
|
+
puts e
|
84
|
+
puts opt_parser
|
85
|
+
exit(1)
|
86
|
+
end
|
87
87
|
|
88
88
|
config = config_path != '' ? YAML.load_file(config_path) : {}
|
89
89
|
config
|
@@ -5,16 +5,15 @@ module SplitIoClient
|
|
5
5
|
class MemoryRepository < EventsRepository
|
6
6
|
EVENTS_SLICE = 100
|
7
7
|
|
8
|
-
def initialize(adapter
|
8
|
+
def initialize(adapter)
|
9
9
|
@adapter = adapter
|
10
|
-
@config = config
|
11
10
|
end
|
12
11
|
|
13
12
|
def add(key, traffic_type, event_type, time, value)
|
14
13
|
@adapter.add_to_queue(m: metadata, e: event(key, traffic_type, event_type, time, value))
|
15
14
|
rescue ThreadError # queue is full
|
16
|
-
if
|
17
|
-
|
15
|
+
if SplitIoClient.configuration.debug_enabled
|
16
|
+
SplitIoClient.configuration.logger.warn("Dropping events. Current size is #{SplitIoClient.configuration.events_queue_size}. " \
|
18
17
|
"Consider increasing events_queue_size")
|
19
18
|
end
|
20
19
|
@adapter.clear
|
@@ -6,13 +6,12 @@ module SplitIoClient
|
|
6
6
|
extend Forwardable
|
7
7
|
def_delegators :@adapter, :add, :clear
|
8
8
|
|
9
|
-
def initialize(adapter
|
10
|
-
@config = config
|
9
|
+
def initialize(adapter)
|
11
10
|
@adapter = case adapter.class.to_s
|
12
11
|
when 'SplitIoClient::Cache::Adapters::MemoryAdapter'
|
13
|
-
Repositories::Events::MemoryRepository.new(adapter
|
12
|
+
Repositories::Events::MemoryRepository.new(adapter)
|
14
13
|
when 'SplitIoClient::Cache::Adapters::RedisAdapter'
|
15
|
-
Repositories::Events::RedisRepository.new(adapter
|
14
|
+
Repositories::Events::RedisRepository.new(adapter)
|
16
15
|
end
|
17
16
|
end
|
18
17
|
|
@@ -20,9 +19,9 @@ module SplitIoClient
|
|
20
19
|
|
21
20
|
def metadata
|
22
21
|
{
|
23
|
-
s: "#{
|
24
|
-
i:
|
25
|
-
n:
|
22
|
+
s: "#{SplitIoClient.configuration.language}-#{SplitIoClient.configuration.version}",
|
23
|
+
i: SplitIoClient.configuration.machine_ip,
|
24
|
+
n: SplitIoClient.configuration.machine_name
|
26
25
|
}
|
27
26
|
end
|
28
27
|
|
@@ -4,9 +4,8 @@ module SplitIoClient
|
|
4
4
|
module Impressions
|
5
5
|
class MemoryRepository
|
6
6
|
|
7
|
-
def initialize(adapter
|
7
|
+
def initialize(adapter)
|
8
8
|
@adapter = adapter
|
9
|
-
@config = config
|
10
9
|
end
|
11
10
|
|
12
11
|
# Store impression data in the selected adapter
|
@@ -14,7 +13,7 @@ module SplitIoClient
|
|
14
13
|
@adapter.add_to_queue(feature: split_name, impressions: data)
|
15
14
|
rescue ThreadError # queue is full
|
16
15
|
if random_sampler.rand(1..1000) <= 2 # log only 0.2 % of the time
|
17
|
-
|
16
|
+
SplitIoClient.configuration.logger.warn("Dropping impressions. Current size is #{SplitIoClient.configuration.impressions_queue_size}. " \
|
18
17
|
"Consider increasing impressions_queue_size")
|
19
18
|
end
|
20
19
|
end
|
@@ -26,7 +25,7 @@ module SplitIoClient
|
|
26
25
|
'keyName' => key,
|
27
26
|
'bucketingKey' => bucketing_key,
|
28
27
|
'treatment' => treatment[:treatment],
|
29
|
-
'label' =>
|
28
|
+
'label' => SplitIoClient.configuration.labels_enabled ? treatment[:label] : nil,
|
30
29
|
'changeNumber' => treatment[:change_number],
|
31
30
|
'time' => time
|
32
31
|
)
|
@@ -34,9 +33,9 @@ module SplitIoClient
|
|
34
33
|
end
|
35
34
|
|
36
35
|
def get_batch
|
37
|
-
return [] if
|
38
|
-
@adapter.get_batch(
|
39
|
-
impression.update(ip:
|
36
|
+
return [] if SplitIoClient.configuration.impressions_bulk_size == 0
|
37
|
+
@adapter.get_batch(SplitIoClient.configuration.impressions_bulk_size).map do |impression|
|
38
|
+
impression.update(ip: SplitIoClient.configuration.machine_ip)
|
40
39
|
end
|
41
40
|
end
|
42
41
|
|
@@ -4,9 +4,8 @@ module SplitIoClient
|
|
4
4
|
module Impressions
|
5
5
|
class RedisRepository < Repository
|
6
6
|
|
7
|
-
def initialize(adapter
|
7
|
+
def initialize(adapter)
|
8
8
|
@adapter = adapter
|
9
|
-
@config = config
|
10
9
|
end
|
11
10
|
|
12
11
|
# Store impression data in Redis
|
@@ -24,24 +23,24 @@ module SplitIoClient
|
|
24
23
|
'keyName' => key,
|
25
24
|
'bucketingKey' => bucketing_key,
|
26
25
|
'treatment' => treatment[:treatment],
|
27
|
-
'label' =>
|
26
|
+
'label' => SplitIoClient.configuration.labels_enabled ? treatment[:label] : nil,
|
28
27
|
'changeNumber' => treatment[:change_number],
|
29
28
|
'time' => time)
|
30
29
|
end
|
31
30
|
end
|
32
31
|
end
|
33
32
|
|
34
|
-
# Get random impressions from redis in batches of size
|
33
|
+
# Get random impressions from redis in batches of size SplitIoClient.configuration.impressions_bulk_size,
|
35
34
|
# delete fetched impressions afterwards
|
36
35
|
def get_batch
|
37
36
|
impressions = impression_keys.each_with_object([]) do |key, memo|
|
38
37
|
ip = key.split('/')[-2] # 'prefix/sdk_lang/ip/impressions.name' -> ip
|
39
38
|
if ip.nil?
|
40
|
-
|
39
|
+
SplitIoClient.configuration.logger.warn("Impressions IP parse error for key: #{key}")
|
41
40
|
next
|
42
41
|
end
|
43
42
|
split_name = key.split('.').last
|
44
|
-
members = @adapter.random_set_elements(key,
|
43
|
+
members = @adapter.random_set_elements(key, SplitIoClient.configuration.impressions_bulk_size)
|
45
44
|
members.each do |impression|
|
46
45
|
parsed_impression = JSON.parse(impression)
|
47
46
|
|
@@ -53,11 +52,11 @@ module SplitIoClient
|
|
53
52
|
end
|
54
53
|
|
55
54
|
@adapter.delete_from_set(key, members)
|
56
|
-
end
|
57
55
|
|
56
|
+
end
|
58
57
|
impressions
|
59
58
|
rescue StandardError => e
|
60
|
-
|
59
|
+
SplitIoClient.configuration.logger.error("Exception while clearing impressions cache: #{e}")
|
61
60
|
|
62
61
|
[]
|
63
62
|
end
|
@@ -66,9 +65,9 @@ module SplitIoClient
|
|
66
65
|
|
67
66
|
# Get all sets by prefix
|
68
67
|
def impression_keys
|
69
|
-
@adapter.find_sets_by_prefix("#{
|
68
|
+
@adapter.find_sets_by_prefix("#{SplitIoClient.configuration.redis_namespace}/*/impressions.*")
|
70
69
|
rescue StandardError => e
|
71
|
-
|
70
|
+
SplitIoClient.configuration.logger.error("Exception while fetching impression_keys: #{e}")
|
72
71
|
|
73
72
|
[]
|
74
73
|
end
|
@@ -6,13 +6,12 @@ module SplitIoClient
|
|
6
6
|
extend Forwardable
|
7
7
|
def_delegators :@adapter, :add, :add_bulk, :get_batch, :empty?
|
8
8
|
|
9
|
-
def initialize(adapter
|
10
|
-
@config = config
|
9
|
+
def initialize(adapter)
|
11
10
|
@adapter = case adapter.class.to_s
|
12
11
|
when 'SplitIoClient::Cache::Adapters::MemoryAdapter'
|
13
|
-
Repositories::Impressions::MemoryRepository.new(adapter
|
12
|
+
Repositories::Impressions::MemoryRepository.new(adapter)
|
14
13
|
when 'SplitIoClient::Cache::Adapters::RedisAdapter'
|
15
|
-
Repositories::Impressions::RedisRepository.new(adapter
|
14
|
+
Repositories::Impressions::RedisRepository.new(adapter)
|
16
15
|
end
|
17
16
|
end
|
18
17
|
end
|
@@ -3,12 +3,10 @@ module SplitIoClient
|
|
3
3
|
module Repositories
|
4
4
|
module Metrics
|
5
5
|
class MemoryRepository
|
6
|
-
def initialize(_ = nil, adapter
|
6
|
+
def initialize(_ = nil, adapter)
|
7
7
|
@counts = []
|
8
8
|
@latencies = []
|
9
9
|
@gauges = []
|
10
|
-
|
11
|
-
@config = config
|
12
10
|
end
|
13
11
|
|
14
12
|
def add_count(counter, delta)
|
@@ -3,8 +3,7 @@ module SplitIoClient
|
|
3
3
|
module Repositories
|
4
4
|
module Metrics
|
5
5
|
class RedisRepository < Repository
|
6
|
-
def initialize(adapter = nil
|
7
|
-
@config = config
|
6
|
+
def initialize(adapter = nil)
|
8
7
|
@adapter = adapter
|
9
8
|
end
|
10
9
|
|
@@ -17,7 +16,6 @@ module SplitIoClient
|
|
17
16
|
|
18
17
|
def add_latency(operation, time_in_ms, binary_search)
|
19
18
|
prefixed_name = impressions_metrics_key("latency.#{operation}")
|
20
|
-
latencies = @adapter.find_strings_by_prefix(prefixed_name)
|
21
19
|
|
22
20
|
if operation == 'sdk.get_treatment'
|
23
21
|
@adapter.inc("#{prefixed_name}.#{binary_search.add_latency_millis(time_in_ms, true)}")
|
@@ -7,13 +7,12 @@ module SplitIoClient
|
|
7
7
|
def_delegators :@adapter, :add_count, :add_latency, :add_gauge, :counts, :latencies, :gauges,
|
8
8
|
:clear_counts, :clear_latencies, :clear_gauges, :clear
|
9
9
|
|
10
|
-
def initialize(adapter
|
11
|
-
@config = config
|
10
|
+
def initialize(adapter)
|
12
11
|
@adapter = case adapter.class.to_s
|
13
12
|
when 'SplitIoClient::Cache::Adapters::MemoryAdapter'
|
14
|
-
Repositories::Metrics::MemoryRepository.new(adapter
|
13
|
+
Repositories::Metrics::MemoryRepository.new(adapter)
|
15
14
|
when 'SplitIoClient::Cache::Adapters::RedisAdapter'
|
16
|
-
Repositories::Metrics::RedisRepository.new(adapter
|
15
|
+
Repositories::Metrics::RedisRepository.new(adapter)
|
17
16
|
end
|
18
17
|
end
|
19
18
|
end
|
@@ -12,11 +12,11 @@ module SplitIoClient
|
|
12
12
|
protected
|
13
13
|
|
14
14
|
def namespace_key(key = '')
|
15
|
-
"#{
|
15
|
+
"#{SplitIoClient.configuration.redis_namespace}#{key}"
|
16
16
|
end
|
17
17
|
|
18
18
|
def impressions_metrics_key(key)
|
19
|
-
namespace_key("/#{
|
19
|
+
namespace_key("/#{SplitIoClient.configuration.language}-#{SplitIoClient.configuration.version}/#{SplitIoClient.configuration.machine_ip}/#{key}")
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
@@ -6,11 +6,9 @@ module SplitIoClient
|
|
6
6
|
|
7
7
|
attr_reader :adapter
|
8
8
|
|
9
|
-
def initialize(adapter
|
9
|
+
def initialize(adapter)
|
10
10
|
@adapter = adapter
|
11
|
-
@
|
12
|
-
|
13
|
-
@adapter.set_bool(namespace_key('.ready'), false)
|
11
|
+
@adapter.set_bool(namespace_key('.ready'), false) unless SplitIoClient.configuration.mode == :consumer
|
14
12
|
end
|
15
13
|
|
16
14
|
# Receives segment data, adds and removes segements from the store
|
@@ -4,16 +4,14 @@ module SplitIoClient
|
|
4
4
|
module Cache
|
5
5
|
module Repositories
|
6
6
|
class SplitsRepository < Repository
|
7
|
-
SPLITS_SLICE = 10
|
8
|
-
|
9
7
|
attr_reader :adapter
|
10
8
|
|
11
|
-
def initialize(adapter
|
9
|
+
def initialize(adapter)
|
12
10
|
@adapter = adapter
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
unless SplitIoClient.configuration.mode == :consumer
|
12
|
+
@adapter.set_string(namespace_key('.splits.till'), '-1')
|
13
|
+
@adapter.initialize_map(namespace_key('.segments.registered'))
|
14
|
+
end
|
17
15
|
end
|
18
16
|
|
19
17
|
def add_split(split)
|
@@ -26,16 +24,14 @@ module SplitIoClient
|
|
26
24
|
@adapter.delete(namespace_key(".split.#{name}"))
|
27
25
|
end
|
28
26
|
|
29
|
-
def get_splits(names
|
27
|
+
def get_splits(names)
|
30
28
|
splits = {}
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
)
|
38
|
-
end
|
29
|
+
split_names = names.reject(&:empty?).uniq.map { |name| namespace_key(".split.#{name}") }
|
30
|
+
splits.merge!(
|
31
|
+
@adapter
|
32
|
+
.multiple_strings(split_names)
|
33
|
+
.map { |name, data| [name.gsub(namespace_key('.split.'), ''), data] }.to_h
|
34
|
+
)
|
39
35
|
|
40
36
|
splits.map do |name, data|
|
41
37
|
parsed_data = data ? JSON.parse(data, symbolize_names: true) : nil
|
@@ -2,9 +2,8 @@ module SplitIoClient
|
|
2
2
|
class ImpressionRouter
|
3
3
|
attr_reader :router_thread
|
4
4
|
|
5
|
-
def initialize
|
6
|
-
@
|
7
|
-
@listener = config.impression_listener
|
5
|
+
def initialize
|
6
|
+
@listener = SplitIoClient.configuration.impression_listener
|
8
7
|
|
9
8
|
return unless @listener
|
10
9
|
|
@@ -45,12 +44,12 @@ module SplitIoClient
|
|
45
44
|
end
|
46
45
|
|
47
46
|
def router_thread
|
48
|
-
|
47
|
+
SplitIoClient.configuration.threads[:impression_router] = Thread.new do
|
49
48
|
loop do
|
50
49
|
begin
|
51
50
|
@listener.log(@queue.pop)
|
52
51
|
rescue StandardError => error
|
53
|
-
|
52
|
+
SplitIoClient.configuration.log_found_exception(__method__.to_s, error)
|
54
53
|
end
|
55
54
|
end
|
56
55
|
end
|