launchdarkly-server-sdk 6.2.3 → 6.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/lib/ldclient-rb/config.rb +81 -4
  4. data/lib/ldclient-rb/evaluation_detail.rb +67 -8
  5. data/lib/ldclient-rb/file_data_source.rb +9 -300
  6. data/lib/ldclient-rb/impl/big_segments.rb +117 -0
  7. data/lib/ldclient-rb/impl/evaluator.rb +80 -28
  8. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +82 -18
  9. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +212 -0
  10. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +84 -31
  11. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
  12. data/lib/ldclient-rb/impl/repeating_task.rb +47 -0
  13. data/lib/ldclient-rb/impl/util.rb +4 -1
  14. data/lib/ldclient-rb/integrations/consul.rb +7 -0
  15. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -2
  16. data/lib/ldclient-rb/integrations/file_data.rb +108 -0
  17. data/lib/ldclient-rb/integrations/redis.rb +41 -1
  18. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +438 -0
  19. data/lib/ldclient-rb/integrations/test_data.rb +209 -0
  20. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +5 -0
  21. data/lib/ldclient-rb/integrations.rb +2 -51
  22. data/lib/ldclient-rb/interfaces.rb +152 -2
  23. data/lib/ldclient-rb/ldclient.rb +21 -7
  24. data/lib/ldclient-rb/polling.rb +22 -41
  25. data/lib/ldclient-rb/util.rb +1 -1
  26. data/lib/ldclient-rb/version.rb +1 -1
  27. metadata +31 -132
  28. data/.circleci/config.yml +0 -40
  29. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -37
  30. data/.github/ISSUE_TEMPLATE/config.yml +0 -5
  31. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  32. data/.github/pull_request_template.md +0 -21
  33. data/.gitignore +0 -16
  34. data/.hound.yml +0 -2
  35. data/.ldrelease/build-docs.sh +0 -18
  36. data/.ldrelease/circleci/linux/execute.sh +0 -18
  37. data/.ldrelease/circleci/mac/execute.sh +0 -18
  38. data/.ldrelease/circleci/template/build.sh +0 -29
  39. data/.ldrelease/circleci/template/publish.sh +0 -23
  40. data/.ldrelease/circleci/template/set-gem-home.sh +0 -7
  41. data/.ldrelease/circleci/template/test.sh +0 -10
  42. data/.ldrelease/circleci/template/update-version.sh +0 -8
  43. data/.ldrelease/circleci/windows/execute.ps1 +0 -19
  44. data/.ldrelease/config.yml +0 -29
  45. data/.rspec +0 -2
  46. data/.rubocop.yml +0 -600
  47. data/.simplecov +0 -4
  48. data/CHANGELOG.md +0 -367
  49. data/CODEOWNERS +0 -1
  50. data/CONTRIBUTING.md +0 -37
  51. data/Gemfile +0 -3
  52. data/azure-pipelines.yml +0 -51
  53. data/docs/Makefile +0 -26
  54. data/docs/index.md +0 -9
  55. data/launchdarkly-server-sdk.gemspec +0 -45
  56. data/spec/config_spec.rb +0 -63
  57. data/spec/diagnostic_events_spec.rb +0 -165
  58. data/spec/evaluation_detail_spec.rb +0 -135
  59. data/spec/event_sender_spec.rb +0 -197
  60. data/spec/event_summarizer_spec.rb +0 -63
  61. data/spec/events_spec.rb +0 -607
  62. data/spec/expiring_cache_spec.rb +0 -76
  63. data/spec/feature_store_spec_base.rb +0 -213
  64. data/spec/file_data_source_spec.rb +0 -283
  65. data/spec/fixtures/feature.json +0 -37
  66. data/spec/fixtures/feature1.json +0 -36
  67. data/spec/fixtures/user.json +0 -9
  68. data/spec/flags_state_spec.rb +0 -81
  69. data/spec/http_util.rb +0 -132
  70. data/spec/impl/evaluator_bucketing_spec.rb +0 -216
  71. data/spec/impl/evaluator_clause_spec.rb +0 -55
  72. data/spec/impl/evaluator_operators_spec.rb +0 -141
  73. data/spec/impl/evaluator_rule_spec.rb +0 -128
  74. data/spec/impl/evaluator_segment_spec.rb +0 -125
  75. data/spec/impl/evaluator_spec.rb +0 -349
  76. data/spec/impl/evaluator_spec_base.rb +0 -75
  77. data/spec/impl/event_factory_spec.rb +0 -108
  78. data/spec/impl/model/serialization_spec.rb +0 -41
  79. data/spec/in_memory_feature_store_spec.rb +0 -12
  80. data/spec/integrations/consul_feature_store_spec.rb +0 -40
  81. data/spec/integrations/dynamodb_feature_store_spec.rb +0 -103
  82. data/spec/integrations/store_wrapper_spec.rb +0 -276
  83. data/spec/launchdarkly-server-sdk_spec.rb +0 -13
  84. data/spec/launchdarkly-server-sdk_spec_autoloadtest.rb +0 -9
  85. data/spec/ldclient_end_to_end_spec.rb +0 -157
  86. data/spec/ldclient_spec.rb +0 -635
  87. data/spec/newrelic_spec.rb +0 -5
  88. data/spec/polling_spec.rb +0 -120
  89. data/spec/redis_feature_store_spec.rb +0 -121
  90. data/spec/requestor_spec.rb +0 -209
  91. data/spec/segment_store_spec_base.rb +0 -95
  92. data/spec/simple_lru_cache_spec.rb +0 -24
  93. data/spec/spec_helper.rb +0 -9
  94. data/spec/store_spec.rb +0 -10
  95. data/spec/stream_spec.rb +0 -45
  96. data/spec/user_filter_spec.rb +0 -91
  97. data/spec/util_spec.rb +0 -17
  98. data/spec/version_spec.rb +0 -7
@@ -1,55 +1,6 @@
1
1
  require "ldclient-rb/integrations/consul"
2
2
  require "ldclient-rb/integrations/dynamodb"
3
+ require "ldclient-rb/integrations/file_data"
3
4
  require "ldclient-rb/integrations/redis"
5
+ require "ldclient-rb/integrations/test_data"
4
6
  require "ldclient-rb/integrations/util/store_wrapper"
5
-
6
- module LaunchDarkly
7
- #
8
- # Tools for connecting the LaunchDarkly client to other software.
9
- #
10
- module Integrations
11
- #
12
- # Integration with [Consul](https://www.consul.io/).
13
- #
14
- # Note that in order to use this integration, you must first install the gem `diplomat`.
15
- #
16
- # @since 5.5.0
17
- #
18
- module Consul
19
- # code is in ldclient-rb/impl/integrations/consul_impl
20
- end
21
-
22
- #
23
- # Integration with [DynamoDB](https://aws.amazon.com/dynamodb/).
24
- #
25
- # Note that in order to use this integration, you must first install one of the AWS SDK gems: either
26
- # `aws-sdk-dynamodb`, or the full `aws-sdk`.
27
- #
28
- # @since 5.5.0
29
- #
30
- module DynamoDB
31
- # code is in ldclient-rb/impl/integrations/dynamodb_impl
32
- end
33
-
34
- #
35
- # Integration with [Redis](https://redis.io/).
36
- #
37
- # Note that in order to use this integration, you must first install the `redis` and `connection-pool`
38
- # gems.
39
- #
40
- # @since 5.5.0
41
- #
42
- module Redis
43
- # code is in ldclient-rb/impl/integrations/redis_impl
44
- end
45
-
46
- #
47
- # Support code that may be helpful in creating integrations.
48
- #
49
- # @since 5.5.0
50
- #
51
- module Util
52
- # code is in ldclient-rb/integrations/util/
53
- end
54
- end
55
- end
@@ -1,3 +1,4 @@
1
+ require "observer"
1
2
 
2
3
  module LaunchDarkly
3
4
  #
@@ -9,7 +10,7 @@ module LaunchDarkly
9
10
  # client uses the feature store to persist feature flags and related objects received from
10
11
  # the LaunchDarkly service. Implementations must support concurrent access and updates.
11
12
  # For more about how feature stores can be used, see:
12
- # [Using a persistent feature store](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store).
13
+ # [Using a persistent feature store](https://docs.launchdarkly.com/sdk/features/storing-data#ruby).
13
14
  #
14
15
  # An entity that can be stored in a feature store is a hash that can be converted to and from
15
16
  # JSON, and that has at a minimum the following properties: `:key`, a string that is unique
@@ -120,7 +121,8 @@ module LaunchDarkly
120
121
  #
121
122
  # The client has its own standard implementation, which uses either a streaming connection or
122
123
  # polling depending on your configuration. Normally you will not need to use another one
123
- # except for testing purposes. {FileDataSource} provides one such test fixture.
124
+ # except for testing purposes. Two such test fixtures are {LaunchDarkly::Integrations::FileData}
125
+ # and {LaunchDarkly::Integrations::TestData}.
124
126
  #
125
127
  module DataSource
126
128
  #
@@ -149,5 +151,153 @@ module LaunchDarkly
149
151
  def stop
150
152
  end
151
153
  end
154
+
155
+ module BigSegmentStore
156
+ #
157
+ # Returns information about the overall state of the store. This method will be called only
158
+ # when the SDK needs the latest state, so it should not be cached.
159
+ #
160
+ # @return [BigSegmentStoreMetadata]
161
+ #
162
+ def get_metadata
163
+ end
164
+
165
+ #
166
+ # Queries the store for a snapshot of the current segment state for a specific user.
167
+ #
168
+ # The user_hash is a base64-encoded string produced by hashing the user key as defined by
169
+ # the Big Segments specification; the store implementation does not need to know the details
170
+ # of how this is done, because it deals only with already-hashed keys, but the string can be
171
+ # assumed to only contain characters that are valid in base64.
172
+ #
173
+ # The return value should be either a Hash, or nil if the user is not referenced in any big
174
+ # segments. Each key in the Hash is a "segment reference", which is how segments are
175
+ # identified in Big Segment data. This string is not identical to the segment key-- the SDK
176
+ # will add other information. The store implementation should not be concerned with the
177
+ # format of the string. Each value in the Hash is true if the user is explicitly included in
178
+ # the segment, false if the user is explicitly excluded from the segment-- and is not also
179
+ # explicitly included (that is, if both an include and an exclude existed in the data, the
180
+ # include would take precedence). If the user's status in a particular segment is undefined,
181
+ # there should be no key or value for that segment.
182
+ #
183
+ # This Hash may be cached by the SDK, so it should not be modified after it is created. It
184
+ # is a snapshot of the segment membership state at one point in time.
185
+ #
186
+ # @param user_hash [String]
187
+ # @return [Hash] true/false values for Big Segments that reference this user
188
+ #
189
+ def get_membership(user_hash)
190
+ end
191
+
192
+ #
193
+ # Performs any necessary cleanup to shut down the store when the client is being shut down.
194
+ #
195
+ # @return [void]
196
+ #
197
+ def stop
198
+ end
199
+ end
200
+
201
+ #
202
+ # Values returned by {BigSegmentStore#get_metadata}.
203
+ #
204
+ class BigSegmentStoreMetadata
205
+ def initialize(last_up_to_date)
206
+ @last_up_to_date = last_up_to_date
207
+ end
208
+
209
+ # The Unix epoch millisecond timestamp of the last update to the {BigSegmentStore}. It is
210
+ # nil if the store has never been updated.
211
+ #
212
+ # @return [Integer|nil]
213
+ attr_reader :last_up_to_date
214
+ end
215
+
216
+ #
217
+ # Information about the status of a Big Segment store, provided by {BigSegmentStoreStatusProvider}.
218
+ #
219
+ # Big Segments are a specific type of user segments. For more information, read the LaunchDarkly
220
+ # documentation: https://docs.launchdarkly.com/home/users/big-segments
221
+ #
222
+ class BigSegmentStoreStatus
223
+ def initialize(available, stale)
224
+ @available = available
225
+ @stale = stale
226
+ end
227
+
228
+ # True if the Big Segment store is able to respond to queries, so that the SDK can evaluate
229
+ # whether a user is in a segment or not.
230
+ #
231
+ # If this property is false, the store is not able to make queries (for instance, it may not have
232
+ # a valid database connection). In this case, the SDK will treat any reference to a Big Segment
233
+ # as if no users are included in that segment. Also, the {EvaluationReason} associated with
234
+ # with any flag evaluation that references a Big Segment when the store is not available will
235
+ # have a `big_segments_status` of `STORE_ERROR`.
236
+ #
237
+ # @return [Boolean]
238
+ attr_reader :available
239
+
240
+ # True if the Big Segment store is available, but has not been updated within the amount of time
241
+ # specified by {BigSegmentsConfig#stale_after}.
242
+ #
243
+ # This may indicate that the LaunchDarkly Relay Proxy, which populates the store, has stopped
244
+ # running or has become unable to receive fresh data from LaunchDarkly. Any feature flag
245
+ # evaluations that reference a Big Segment will be using the last known data, which may be out
246
+ # of date. Also, the {EvaluationReason} associated with those evaluations will have a
247
+ # `big_segments_status` of `STALE`.
248
+ #
249
+ # @return [Boolean]
250
+ attr_reader :stale
251
+
252
+ def ==(other)
253
+ self.available == other.available && self.stale == other.stale
254
+ end
255
+ end
256
+
257
+ #
258
+ # An interface for querying the status of a Big Segment store.
259
+ #
260
+ # The Big Segment store is the component that receives information about Big Segments, normally
261
+ # from a database populated by the LaunchDarkly Relay Proxy. Big Segments are a specific type
262
+ # of user segments. For more information, read the LaunchDarkly documentation:
263
+ # https://docs.launchdarkly.com/home/users/big-segments
264
+ #
265
+ # An implementation of this interface is returned by {LDClient#big_segment_store_status_provider}.
266
+ # Application code never needs to implement this interface.
267
+ #
268
+ # There are two ways to interact with the status. One is to simply get the current status; if its
269
+ # `available` property is true, then the SDK is able to evaluate user membership in Big Segments,
270
+ # and the `stale`` property indicates whether the data might be out of date.
271
+ #
272
+ # The other way is to subscribe to status change notifications. Applications may wish to know if
273
+ # there is an outage in the Big Segment store, or if it has become stale (the Relay Proxy has
274
+ # stopped updating it with new data), since then flag evaluations that reference a Big Segment
275
+ # might return incorrect values. To allow finding out about status changes as soon as possible,
276
+ # `BigSegmentStoreStatusProvider` mixes in Ruby's
277
+ # [Observable](https://docs.ruby-lang.org/en/2.5.0/Observable.html) module to provide standard
278
+ # methods such as `add_observer`. Observers will be called with a new {BigSegmentStoreStatus}
279
+ # value whenever the status changes.
280
+ #
281
+ # @example Getting the current status
282
+ # status = client.big_segment_store_status_provider.status
283
+ #
284
+ # @example Subscribing to status notifications
285
+ # client.big_segment_store_status_provider.add_observer(self, :big_segments_status_changed)
286
+ #
287
+ # def big_segments_status_changed(new_status)
288
+ # puts "Big segment store status is now: #{new_status}"
289
+ # end
290
+ #
291
+ module BigSegmentStoreStatusProvider
292
+ include Observable
293
+ #
294
+ # Gets the current status of the store, if known.
295
+ #
296
+ # @return [BigSegmentStoreStatus] the status, or nil if the SDK has not yet queried the Big
297
+ # Segment store status
298
+ #
299
+ def status
300
+ end
301
+ end
152
302
  end
153
303
  end
@@ -1,3 +1,4 @@
1
+ require "ldclient-rb/impl/big_segments"
1
2
  require "ldclient-rb/impl/diagnostic_events"
2
3
  require "ldclient-rb/impl/evaluator"
3
4
  require "ldclient-rb/impl/event_factory"
@@ -57,10 +58,14 @@ module LaunchDarkly
57
58
  updated_config.instance_variable_set(:@feature_store, @store)
58
59
  @config = updated_config
59
60
 
61
+ @big_segment_store_manager = Impl::BigSegmentStoreManager.new(config.big_segments, @config.logger)
62
+ @big_segment_store_status_provider = @big_segment_store_manager.status_provider
63
+
60
64
  get_flag = lambda { |key| @store.get(FEATURES, key) }
61
65
  get_segment = lambda { |key| @store.get(SEGMENTS, key) }
62
- @evaluator = LaunchDarkly::Impl::Evaluator.new(get_flag, get_segment, @config.logger)
63
-
66
+ get_big_segments_membership = lambda { |key| @big_segment_store_manager.get_user_membership(key) }
67
+ @evaluator = LaunchDarkly::Impl::Evaluator.new(get_flag, get_segment, get_big_segments_membership, @config.logger)
68
+
64
69
  if !@config.offline? && @config.send_events && !@config.diagnostic_opt_out?
65
70
  diagnostic_accumulator = Impl::DiagnosticAccumulator.new(Impl::DiagnosticAccumulator.create_diagnostic_id(sdk_key))
66
71
  else
@@ -132,7 +137,7 @@ module LaunchDarkly
132
137
 
133
138
  #
134
139
  # Creates a hash string that can be used by the JavaScript SDK to identify a user.
135
- # For more information, see [Secure mode](https://docs.launchdarkly.com/docs/js-sdk-reference#section-secure-mode).
140
+ # For more information, see [Secure mode](https://docs.launchdarkly.com/sdk/features/secure-mode#ruby).
136
141
  #
137
142
  # @param user [Hash] the user properties
138
143
  # @return [String] a hash string
@@ -172,11 +177,11 @@ module LaunchDarkly
172
177
  #
173
178
  # Other supported user attributes include IP address, country code, and an arbitrary hash of
174
179
  # custom attributes. For more about the supported user properties and how they work in
175
- # LaunchDarkly, see [Targeting users](https://docs.launchdarkly.com/docs/targeting-users).
180
+ # LaunchDarkly, see [Targeting users](https://docs.launchdarkly.com/home/flags/targeting-users).
176
181
  #
177
182
  # The optional `:privateAttributeNames` user property allows you to specify a list of
178
183
  # attribute names that should not be sent back to LaunchDarkly.
179
- # [Private attributes](https://docs.launchdarkly.com/docs/private-user-attributes)
184
+ # [Private attributes](https://docs.launchdarkly.com/home/users/attributes#creating-private-user-attributes)
180
185
  # can also be configured globally in {Config}.
181
186
  #
182
187
  # @example Basic user hash
@@ -213,7 +218,7 @@ module LaunchDarkly
213
218
  # be included in analytics events, if you are capturing detailed event data for this flag.
214
219
  #
215
220
  # For more information, see the reference guide on
216
- # [Evaluation reasons](https://docs.launchdarkly.com/v2.0/docs/evaluation-reasons).
221
+ # [Evaluation reasons](https://docs.launchdarkly.com/sdk/concepts/evaluation-reasons).
217
222
  #
218
223
  # @param key [String] the unique feature key for the feature flag, as shown
219
224
  # on the LaunchDarkly dashboard
@@ -260,7 +265,7 @@ module LaunchDarkly
260
265
  #
261
266
  # As of this version’s release date, the LaunchDarkly service does not support the `metricValue`
262
267
  # parameter. As a result, specifying `metricValue` will not yet produce any different behavior
263
- # from omitting it. Refer to the [SDK reference guide](https://docs.launchdarkly.com/docs/ruby-sdk-reference#section-track)
268
+ # from omitting it. Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/features/events#ruby)
264
269
  # for the latest status.
265
270
  #
266
271
  # @param event_name [String] The name of the event
@@ -375,9 +380,18 @@ module LaunchDarkly
375
380
  @config.logger.info { "[LDClient] Closing LaunchDarkly client..." }
376
381
  @data_source.stop
377
382
  @event_processor.stop
383
+ @big_segment_store_manager.stop
378
384
  @store.stop
379
385
  end
380
386
 
387
+ #
388
+ # Returns an interface for tracking the status of a Big Segment store.
389
+ #
390
+ # The {BigSegmentStoreStatusProvider} has methods for checking whether the Big Segment store
391
+ # is (as far as the SDK knows) currently operational and tracking changes in this status.
392
+ #
393
+ attr_reader :big_segment_store_status_provider
394
+
381
395
  private
382
396
 
383
397
  def create_default_data_source(sdk_key, config, diagnostic_accumulator)
@@ -1,3 +1,5 @@
1
+ require "ldclient-rb/impl/repeating_task"
2
+
1
3
  require "concurrent/atomics"
2
4
  require "thread"
3
5
 
@@ -9,8 +11,8 @@ module LaunchDarkly
9
11
  @requestor = requestor
10
12
  @initialized = Concurrent::AtomicBoolean.new(false)
11
13
  @started = Concurrent::AtomicBoolean.new(false)
12
- @stopped = Concurrent::AtomicBoolean.new(false)
13
14
  @ready = Concurrent::Event.new
15
+ @task = Impl::RepeatingTask.new(@config.poll_interval, 0, -> { self.poll }, @config.logger)
14
16
  end
15
17
 
16
18
  def initialized?
@@ -20,56 +22,35 @@ module LaunchDarkly
20
22
  def start
21
23
  return @ready unless @started.make_true
22
24
  @config.logger.info { "[LDClient] Initializing polling connection" }
23
- create_worker
25
+ @task.start
24
26
  @ready
25
27
  end
26
28
 
27
29
  def stop
28
- if @stopped.make_true
29
- if @worker && @worker.alive? && @worker != Thread.current
30
- @worker.run # causes the thread to wake up if it's currently in a sleep
31
- @worker.join
32
- end
33
- @config.logger.info { "[LDClient] Polling connection stopped" }
34
- end
30
+ @task.stop
31
+ @config.logger.info { "[LDClient] Polling connection stopped" }
35
32
  end
36
33
 
37
34
  def poll
38
- all_data = @requestor.request_all_data
39
- if all_data
40
- @config.feature_store.init(all_data)
41
- if @initialized.make_true
42
- @config.logger.info { "[LDClient] Polling connection initialized" }
43
- @ready.set
44
- end
45
- end
46
- end
47
-
48
- def create_worker
49
- @worker = Thread.new do
50
- @config.logger.debug { "[LDClient] Starting polling worker" }
51
- while !@stopped.value do
52
- started_at = Time.now
53
- begin
54
- poll
55
- rescue UnexpectedResponseError => e
56
- message = Util.http_error_message(e.status, "polling request", "will retry")
57
- @config.logger.error { "[LDClient] #{message}" };
58
- if !Util.http_error_recoverable?(e.status)
59
- @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
60
- stop
61
- end
62
- rescue StandardError => exn
63
- Util.log_exception(@config.logger, "Exception while polling", exn)
64
- end
65
- delta = @config.poll_interval - (Time.now - started_at)
66
- if delta > 0
67
- sleep(delta)
35
+ begin
36
+ all_data = @requestor.request_all_data
37
+ if all_data
38
+ @config.feature_store.init(all_data)
39
+ if @initialized.make_true
40
+ @config.logger.info { "[LDClient] Polling connection initialized" }
41
+ @ready.set
68
42
  end
69
43
  end
44
+ rescue UnexpectedResponseError => e
45
+ message = Util.http_error_message(e.status, "polling request", "will retry")
46
+ @config.logger.error { "[LDClient] #{message}" };
47
+ if !Util.http_error_recoverable?(e.status)
48
+ @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
49
+ stop
50
+ end
51
+ rescue StandardError => e
52
+ Util.log_exception(@config.logger, "Exception while polling", e)
70
53
  end
71
54
  end
72
-
73
- private :poll, :create_worker
74
55
  end
75
56
  end
@@ -18,7 +18,7 @@ module LaunchDarkly
18
18
  end
19
19
  ret
20
20
  end
21
-
21
+
22
22
  def self.new_http_client(uri_s, config)
23
23
  http_client_options = {}
24
24
  if config.socket_factory
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "6.2.3"
2
+ VERSION = "6.3.1"
3
3
  end