ldclient-rb 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9e44a1aa86a1b8486920b0798f25160481a8fd06
4
- data.tar.gz: 0ecdeb68b3479534fc148dc277181fd4d98d94c3
3
+ metadata.gz: abbd939adce22e127e3b1dc732d5d24487457a79
4
+ data.tar.gz: a903a1c807ce38b74c207ef53196198d5d16f489
5
5
  SHA512:
6
- metadata.gz: 47fa2c0003a580ce85eb90a235a1405723fd891bef78bfff21cfa9cc84933b1f0d24dc228ccda1dd1a97d973426754bc246e2b0d80f26f0272b2e0c2d589f127
7
- data.tar.gz: e4dbca380db1a33754d595b98a17969981876c0d04ea36d275c8aec95f0de9d2abe519a3ac1d443964f4c399ad7e7bc3e6eb63f167123f9de347603820d5d3c4
6
+ metadata.gz: 85400dc792fbe9f4ddb982cc195b0342ac74fb2e97203a910242b4b72376d1fa28522a5468c6ee9861167a4fe6e06ab4aeeac131e3f043973d6fbe2130aa7754
7
+ data.tar.gz: 8c1a5de8549b1b26aa4aae92b716a06740e9bdd615c4a9bdc06d763800076e1a579068fc899fd1662ee600d1635f789eb73e5930214d8af1e8a57dfd5f78cbca
data/.gitignore CHANGED
@@ -11,4 +11,5 @@
11
11
  *.o
12
12
  *.a
13
13
  mkmf.log
14
- *.gem
14
+ *.gem
15
+ Gemfile.lock
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in ldclient-rb.gemspec
4
3
  gemspec
data/README.md CHANGED
@@ -32,4 +32,9 @@ Your first feature flag
32
32
  Learn more
33
33
  -----------
34
34
 
35
- Check out our [documentation](http://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](http://docs.launchdarkly.com/v1.0/docs/ruby-sdk-reference).
35
+ Check out our [documentation](http://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](http://docs.launchdarkly.com/v1.0/docs/ruby-sdk-reference).
36
+
37
+ Contributing
38
+ ------------
39
+
40
+ We encourage pull-requests and other contributions from the community. We've also published an [SDK contributor's guide](http://docs.launchdarkly.com/v1.0/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work.
@@ -6,11 +6,11 @@ require 'ldclient-rb/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "ldclient-rb"
8
8
  spec.version = LaunchDarkly::VERSION
9
- spec.authors = ["Catamorphic Co"]
10
- spec.email = ["team@catamorphic.com"]
9
+ spec.authors = ["LaunchDarkly"]
10
+ spec.email = ["team@launchdarkly.com"]
11
11
  spec.summary = %q{LaunchDarkly SDK for Ruby}
12
12
  spec.description = %q{Official LaunchDarkly SDK for Ruby}
13
- spec.homepage = ""
13
+ spec.homepage = "https://rubygems.org/gems/ldclient-rb"
14
14
  spec.license = "Apache 2.0"
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0")
@@ -25,5 +25,8 @@ Gem::Specification.new do |spec|
25
25
  spec.add_runtime_dependency "faraday", "~> 0.9"
26
26
  spec.add_runtime_dependency "faraday-http-cache", "~> 0.4"
27
27
  spec.add_runtime_dependency "thread_safe", "~> 0.3"
28
- spec.add_runtime_dependency "net-http-persistent", "~> 2.9.4"
28
+ spec.add_runtime_dependency "net-http-persistent", "~> 2.9"
29
+ spec.add_runtime_dependency "concurrent-ruby", "~> 0.9"
30
+ spec.add_runtime_dependency "hashdiff", "~> 0.2"
31
+ spec.add_runtime_dependency "ld-em-eventsource", "~> 0.2"
29
32
  end
@@ -2,4 +2,5 @@ require "ldclient-rb/version"
2
2
  require "ldclient-rb/ldclient"
3
3
  require "ldclient-rb/store"
4
4
  require "ldclient-rb/config"
5
- require "ldclient-rb/newrelic"
5
+ require "ldclient-rb/newrelic"
6
+ require "ldclient-rb/stream"
@@ -23,6 +23,7 @@ module LaunchDarkly
23
23
  # @return [type] [description]
24
24
  def initialize(opts = {})
25
25
  @base_uri = (opts[:base_uri] || Config.default_base_uri).chomp("/")
26
+ @stream_uri = (opts[:stream_uri] || Config.default_stream_uri).chomp("/")
26
27
  @capacity = opts[:capacity] || Config.default_capacity
27
28
  @logger = opts[:logger] || Config.default_logger
28
29
  @store = opts[:store] || Config.default_store
@@ -30,6 +31,9 @@ module LaunchDarkly
30
31
  @connect_timeout = opts[:connect_timeout] || Config.default_connect_timeout
31
32
  @read_timeout = opts[:read_timeout] || Config.default_read_timeout
32
33
  @log_timings = opts[:log_timings] || Config.default_log_timings
34
+ @stream = opts[:stream] || Config.default_stream
35
+ @feature_store = opts[:feature_store] || Config.default_feature_store
36
+ @debug_stream = opts[:debug_stream] || Config.default_debug_stream
33
37
  end
34
38
 
35
39
  #
@@ -40,6 +44,32 @@ module LaunchDarkly
40
44
  @base_uri
41
45
  end
42
46
 
47
+ #
48
+ # The base URL for the LaunchDarkly streaming server.
49
+ #
50
+ # @return [String] The configured base URL for the LaunchDarkly streaming server.
51
+ def stream_uri
52
+ @stream_uri
53
+ end
54
+
55
+ #
56
+ # Whether streaming mode should be enabled. Streaming mode asynchronously updates
57
+ # feature flags in real-time using server-sent events.
58
+ #
59
+ # @return [Boolean] True if streaming mode should be enabled
60
+ def stream?
61
+ @stream
62
+ end
63
+
64
+ #
65
+ # Whether we should debug streaming mode. If set, the client will fetch features via polling
66
+ # and compare the retrieved feature with the value in the feature store
67
+ #
68
+ # @return [Boolean] True if we should debug streaming mode
69
+ def debug_stream?
70
+ @debug_stream
71
+ end
72
+
43
73
  #
44
74
  # The number of seconds between flushes of the event buffer. Decreasing the flush interval means
45
75
  # that the event buffer is less likely to reach capacity.
@@ -100,6 +130,13 @@ module LaunchDarkly
100
130
  @log_timings
101
131
  end
102
132
 
133
+ #
134
+ # TODO docs
135
+ #
136
+ def feature_store
137
+ @feature_store
138
+ end
139
+
103
140
  #
104
141
  # The default LaunchDarkly client configuration. This configuration sets reasonable defaults for most users.
105
142
  #
@@ -116,6 +153,10 @@ module LaunchDarkly
116
153
  "https://app.launchdarkly.com"
117
154
  end
118
155
 
156
+ def self.default_stream_uri
157
+ "https://stream.launchdarkly.com"
158
+ end
159
+
119
160
  def self.default_store
120
161
  defined?(Rails) && Rails.respond_to?(:cache) ? Rails.cache : ThreadSafeMemoryStore.new
121
162
  end
@@ -140,5 +181,17 @@ module LaunchDarkly
140
181
  false
141
182
  end
142
183
 
184
+ def self.default_stream
185
+ false
186
+ end
187
+
188
+ def self.default_feature_store
189
+ nil
190
+ end
191
+
192
+ def self.default_debug_stream
193
+ false
194
+ end
195
+
143
196
  end
144
197
  end
@@ -5,6 +5,7 @@ require 'thread'
5
5
  require 'logger'
6
6
  require 'net/http/persistent'
7
7
  require 'benchmark'
8
+ require 'hashdiff'
8
9
 
9
10
  module LaunchDarkly
10
11
 
@@ -38,6 +39,10 @@ module LaunchDarkly
38
39
  end
39
40
  @offline = false
40
41
 
42
+ if @config.stream?
43
+ @stream_processor = StreamProcessor.new(api_key, config)
44
+ end
45
+
41
46
  @worker = create_worker()
42
47
  end
43
48
 
@@ -51,7 +56,6 @@ module LaunchDarkly
51
56
  rescue
52
57
  end
53
58
 
54
-
55
59
  if !events.empty?()
56
60
  res = log_timings("Flush events") {
57
61
  next @client.post (@config.base_uri + "/api/events/bulk") do |req|
@@ -124,12 +128,35 @@ module LaunchDarkly
124
128
  return default
125
129
  end
126
130
 
127
- value = get_flag_int(key, user, default)
131
+ unless user
132
+ @config.logger.error("[LDClient] Must specify user")
133
+ return default
134
+ end
135
+
136
+ if @config.stream? and not @stream_processor.started?
137
+ @stream_processor.start
138
+ end
139
+
140
+ if @config.stream? and @stream_processor.initialized?
141
+ feature = get_flag_stream(key)
142
+ if @config.debug_stream?
143
+ polled = get_flag_int(key)
144
+ diff = HashDiff.diff(feature, polled)
145
+ if not diff.empty?
146
+ @config.logger.error("Streamed flag differs from polled flag " + diff.to_s)
147
+ end
148
+ end
149
+ else
150
+ feature = get_flag_int(key)
151
+ end
152
+ value = evaluate(feature, user)
153
+ value.nil? ? default : value
154
+
128
155
  add_event({:kind => 'feature', :key => key, :user => user, :value => value})
129
156
  LDNewRelic.annotate_transaction(key, value)
130
157
  return value
131
158
  rescue StandardError => error
132
- @config.logger.error("[LDClient] Unhandled exception in get_flag: (#{error.class.name}) #{error.to_s}\n\t#{error.backtrace.join("\n\t")}")
159
+ @config.logger.error("[LDClient] Unhandled exception in toggle: (#{error.class.name}) #{error.to_s}\n\t#{error.backtrace.join("\n\t")}")
133
160
  default
134
161
  end
135
162
  end
@@ -142,7 +169,7 @@ module LaunchDarkly
142
169
  event[:creationDate] = (Time.now.to_f * 1000).to_i
143
170
  @queue.push(event)
144
171
 
145
- if ! @worker.alive?
172
+ if !@worker.alive?
146
173
  @worker = create_worker()
147
174
  end
148
175
  else
@@ -175,7 +202,7 @@ module LaunchDarkly
175
202
  # Tracks that a user performed an event
176
203
  #
177
204
  # @param event_name [String] The name of the event
178
- # @param user [Hash] The user that performed the event. This should be the same user hash used in calls to {#get_flag?}
205
+ # @param user [Hash] The user that performed the event. This should be the same user hash used in calls to {#toggle?}
179
206
  # @param data [Hash] A hash containing any additional data associated with the event
180
207
  #
181
208
  # @return [void]
@@ -183,14 +210,37 @@ module LaunchDarkly
183
210
  add_event({:kind => 'custom', :key => event_name, :user => user, :data => data })
184
211
  end
185
212
 
186
- def get_flag_int(key, user, default)
213
+ #
214
+ # Returns the key of every feature
215
+ #
216
+ def feature_keys
217
+ get_features.map {|feature| feature[:key]}
218
+ end
187
219
 
188
- unless user
189
- @config.logger.error("[LDClient] Must specify user")
190
- return default
220
+ #
221
+ # Returns all features
222
+ #
223
+ def get_features
224
+ res = @client.get (@config.base_uri + '/api/features') do |req|
225
+ req.headers['Authorization'] = 'api_key ' + @api_key
226
+ req.headers['User-Agent'] = 'RubyClient/' + LaunchDarkly::VERSION
227
+ req.options.timeout = @config.read_timeout
228
+ req.options.open_timeout = @config.connect_timeout
191
229
  end
192
230
 
193
- res = log_timings("Flush events") {
231
+ if res.status == 200 then
232
+ return JSON.parse(res.body, symbolize_names: true)[:items]
233
+ else
234
+ @config.logger.error("[LDClient] Unexpected status code #{res.status}")
235
+ end
236
+ end
237
+
238
+ def get_flag_stream(key)
239
+ @stream_processor.get_feature(key)
240
+ end
241
+
242
+ def get_flag_int(key)
243
+ res = log_timings("Feature request") {
194
244
  next @client.get (@config.base_uri + '/api/eval/features/' + key) do |req|
195
245
  req.headers['Authorization'] = 'api_key ' + @api_key
196
246
  req.headers['User-Agent'] = 'RubyClient/' + LaunchDarkly::VERSION
@@ -201,25 +251,21 @@ module LaunchDarkly
201
251
 
202
252
  if res.status == 401
203
253
  @config.logger.error("[LDClient] Invalid API key")
204
- return default
254
+ return nil
205
255
  end
206
256
 
207
257
  if res.status == 404
208
258
  @config.logger.error("[LDClient] Unknown feature key: #{key}")
209
- return default
259
+ return nil
210
260
  end
211
261
 
212
262
  if res.status != 200
213
263
  @config.logger.error("[LDClient] Unexpected status code #{res.status}")
214
- return default
264
+ return nil
215
265
  end
216
266
 
217
267
 
218
- feature = JSON.parse(res.body, :symbolize_names => true)
219
-
220
- val = evaluate(feature, user)
221
-
222
- val == nil ? default : val
268
+ JSON.parse(res.body, :symbolize_names => true)
223
269
  end
224
270
 
225
271
  def param_for_user(feature, user)
@@ -289,13 +335,13 @@ module LaunchDarkly
289
335
  end
290
336
 
291
337
  def evaluate(feature, user)
292
- unless feature[:on]
338
+ if feature.nil? || !feature[:on]
293
339
  return nil
294
340
  end
295
341
 
296
342
  param = param_for_user(feature, user)
297
343
 
298
- if param == nil
344
+ if param.nil?
299
345
  return nil
300
346
  end
301
347
 
@@ -344,7 +390,7 @@ module LaunchDarkly
344
390
  return res
345
391
  end
346
392
 
347
- private :add_event, :get_flag_int, :param_for_user, :match_target?, :match_user?, :match_variation?, :evaluate, :create_worker, :log_timings
393
+ private :add_event, :get_flag_stream, :get_flag_int, :param_for_user, :match_target?, :match_user?, :match_variation?, :evaluate, :create_worker, :log_timings
348
394
 
349
395
  end
350
396
  end
@@ -0,0 +1,166 @@
1
+ require 'concurrent/atomics'
2
+ require 'json'
3
+ require 'ld-em-eventsource'
4
+
5
+ module LaunchDarkly
6
+
7
+ PUT = "put"
8
+ PATCH = "patch"
9
+ DELETE = "delete"
10
+
11
+ class InMemoryFeatureStore
12
+ def initialize()
13
+ @features = Hash.new
14
+ @lock = Concurrent::ReadWriteLock.new
15
+ @initialized = Concurrent::AtomicBoolean.new(false)
16
+ end
17
+
18
+ def get(key)
19
+ @lock.with_read_lock {
20
+ f = @features[key.to_sym]
21
+ (f.nil? || f[:deleted]) ? nil : f
22
+ }
23
+ end
24
+
25
+ def all()
26
+ @lock.with_read_lock {
27
+ @features.select {|k,f| not f[:deleted]}
28
+ }
29
+ end
30
+
31
+ def delete(key, version)
32
+ @lock.with_write_lock {
33
+ old = @features[key.to_sym]
34
+
35
+ if old != nil and old[:version] < version
36
+ old[:deleted] = true
37
+ old[:version] = version
38
+ @features[key.to_sym] = old
39
+ elsif old.nil?
40
+ @features[key.to_sym] = {:deleted => true, :version => version}
41
+ end
42
+ }
43
+ end
44
+
45
+ def init(fs)
46
+ @lock.with_write_lock {
47
+ @features.replace(fs)
48
+ @initialized.make_true
49
+ }
50
+ end
51
+
52
+ def upsert(key, feature)
53
+ @lock.with_write_lock {
54
+ old = @features[key.to_sym]
55
+
56
+ if old.nil? or old[:version] < feature[:version]
57
+ @features[key.to_sym] = feature
58
+ end
59
+ }
60
+ end
61
+
62
+ def initialized?()
63
+ @initialized.value
64
+ end
65
+ end
66
+
67
+ class StreamProcessor
68
+ def initialize(api_key, config)
69
+ @api_key = api_key
70
+ @config = config
71
+ @store = config.feature_store ? config.feature_store : InMemoryFeatureStore.new
72
+ @disconnected = Concurrent::AtomicReference.new(nil)
73
+ @started = Concurrent::AtomicBoolean.new(false)
74
+ end
75
+
76
+ def initialized?()
77
+ @store.initialized?
78
+ end
79
+
80
+ def started?()
81
+ @started.value
82
+ end
83
+
84
+ def get_feature(key)
85
+ if not initialized?
86
+ throw :uninitialized
87
+ end
88
+ @store.get(key)
89
+ end
90
+
91
+ def start_reactor()
92
+ if defined?(Thin)
93
+ @config.logger.debug("Running in a Thin environment-- not starting EventMachine")
94
+ elsif EM.reactor_running?
95
+ @config.logger.debug("EventMachine already running")
96
+ else
97
+ @config.logger.debug("Starting EventMachine")
98
+ Thread.new { EM.run {} }
99
+ Thread.pass until EM.reactor_running?
100
+ end
101
+ EM.reactor_running?
102
+ end
103
+
104
+ def start()
105
+ # Try to start the reactor. If it's not started, we shouldn't start
106
+ # the stream processor
107
+ if not start_reactor
108
+ return
109
+ end
110
+
111
+ # If someone else booted the stream processor connection, just return
112
+ if not @started.make_true
113
+ return
114
+ end
115
+
116
+ # If we're the first and only thread to set started, boot
117
+ # the stream processor connection
118
+ EM.defer do
119
+ source = EM::EventSource.new(@config.stream_uri + "/features",
120
+ {},
121
+ {'Accept' => 'text/event-stream',
122
+ 'Authorization' => 'api_key ' + @api_key,
123
+ 'User-Agent' => 'RubyClient/' + LaunchDarkly::VERSION})
124
+ source.on PUT do |message|
125
+ features = JSON.parse(message, :symbolize_names => true)
126
+ @store.init(features)
127
+ set_connected
128
+ end
129
+ source.on PATCH do |message|
130
+ json = JSON.parse(message, :symbolize_names => true)
131
+ @store.upsert(json[:path][1..-1], json[:data])
132
+ set_connected
133
+ end
134
+ source.on DELETE do |message|
135
+ json = JSON.parse(message, :symbolize_names => true)
136
+ @store.delete(json[:path][1..-1], json[:version])
137
+ set_connected
138
+ end
139
+ source.error do |error|
140
+ @config.logger.error("[LDClient] Error subscribing to stream API: #{error}")
141
+ set_disconnected
142
+ end
143
+ source.inactivity_timeout = 0
144
+ source.start
145
+ end
146
+ end
147
+
148
+ def set_disconnected()
149
+ @disconnected.set(Time.now)
150
+ end
151
+
152
+ def set_connected()
153
+ @disconnected.set(nil)
154
+ end
155
+
156
+ def should_fallback_update()
157
+ disc = @disconnected.get
158
+ disc != nil and disc < (Time.now - 120)
159
+ end
160
+
161
+ # TODO mark private methods
162
+ private :set_connected, :set_disconnected, :start_reactor
163
+
164
+ end
165
+
166
+ end
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ldclient-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
- - Catamorphic Co
7
+ - LaunchDarkly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-01 00:00:00.000000000 Z
11
+ date: 2015-09-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -100,24 +100,65 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 2.9.4
103
+ version: '2.9'
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 2.9.4
110
+ version: '2.9'
111
+ - !ruby/object:Gem::Dependency
112
+ name: concurrent-ruby
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.9'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.9'
125
+ - !ruby/object:Gem::Dependency
126
+ name: hashdiff
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.2'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.2'
139
+ - !ruby/object:Gem::Dependency
140
+ name: ld-em-eventsource
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.2'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.2'
111
153
  description: Official LaunchDarkly SDK for Ruby
112
154
  email:
113
- - team@catamorphic.com
155
+ - team@launchdarkly.com
114
156
  executables: []
115
157
  extensions: []
116
158
  extra_rdoc_files: []
117
159
  files:
118
160
  - ".gitignore"
119
161
  - Gemfile
120
- - Gemfile.lock
121
162
  - LICENSE.txt
122
163
  - README.md
123
164
  - Rakefile
@@ -127,8 +168,9 @@ files:
127
168
  - lib/ldclient-rb/ldclient.rb
128
169
  - lib/ldclient-rb/newrelic.rb
129
170
  - lib/ldclient-rb/store.rb
171
+ - lib/ldclient-rb/stream.rb
130
172
  - lib/ldclient-rb/version.rb
131
- homepage: ''
173
+ homepage: https://rubygems.org/gems/ldclient-rb
132
174
  licenses:
133
175
  - Apache 2.0
134
176
  metadata: {}
@@ -1,30 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- ldclient-rb (0.1.0)
5
- faraday (~> 0.9)
6
- faraday-http-cache (~> 0.4)
7
- json (~> 1.8)
8
- net-http-persistent (~> 2.9.4)
9
- thread_safe (~> 0.3)
10
-
11
- GEM
12
- remote: https://rubygems.org/
13
- specs:
14
- faraday (0.9.1)
15
- multipart-post (>= 1.2, < 3)
16
- faraday-http-cache (0.4.2)
17
- faraday (~> 0.8)
18
- json (1.8.2)
19
- multipart-post (2.0.0)
20
- net-http-persistent (2.9.4)
21
- rake (10.4.2)
22
- thread_safe (0.3.4)
23
-
24
- PLATFORMS
25
- ruby
26
-
27
- DEPENDENCIES
28
- bundler (~> 1.7)
29
- ldclient-rb!
30
- rake (~> 10.0)